Skip to content

Conversation

@OzGav
Copy link
Contributor

@OzGav OzGav commented Oct 11, 2025

My first attempt at a plugin!

This creates a DLNA server which allows playback of local file system files.

In my testing I tried:
Android BubbleUPNP: Worked very well allowing queueing of all artist tracks, or all album tracks or individual tracks
VLC on WIndows: Allowed playback of individual tracks
nPlayer Lite on iOS: Played back individual tracks and albums.

Overall, the plugin seemed to be working and variations in functionality I assess are due to the respective clients. BubbleUPNP for example provides a rich experience with full artwork and full capabilities.

If it is possible to serve the streaming provider content over DLNA I am open to pointers!

Hopefully satisfies https://github.com/orgs/music-assistant/discussions/388

@MarvinSchenkel
Copy link
Contributor

Just had a little play with this. Nice job!

A few things I noticed:

  • Works really well playing local/smb file sources.
  • Also works in the dev addon via HA ingress!

Now, it also seems to index media from streaming providers. When opening the 'Artist' folder, it seems to call get_streamdetails() for every track in there, causes a long delay for YTM for example, since resolving streamurls takes 1-2sec per Track. Playing any of the Tracks coming from a streaming provider results in an error.

I think the goal here is to only serve the content from the local/smb file providers? If so, can we configure the plugin to only advertise media items that are provided by local/smb?

@OzGav
Copy link
Contributor Author

OzGav commented Oct 14, 2025

@MarvinSchenkel I think I have to do something. What do you think of only scanning at the track level so you would see the artist then album but inside would be empty if there were no local tracks? Otherwise I think it could be very slow if the provider had to scan every track of an artist just in case one had a local mapping?

@MarvinSchenkel
Copy link
Contributor

MarvinSchenkel commented Oct 14, 2025

@MarvinSchenkel I think I have to do something. What do you think of only scanning at the track level so you would see the artist then album but inside would be empty if there were no local tracks? Otherwise I think it could be very slow if the provider had to scan every track of an artist just in case one had a local mapping?

The library functions should have a provider parameter that allows you to filter on provider. Something like this should work:

Add a constant on the top:
SUPPORTED_PROVIDERS = ("filesystem", "filesystem_smb")

Then inside _get_children(), this should work for artists

artists = []
for provider in SUPPORTED_PROVIDERS:
  artists.extend(await self.mass.music.artists.library_items(
      limit=limit, offset=offset, order_by="sort_name",  provider=provider
  ))
artist_items = [self._create_artist_container(artist) for artist in artists]
total = len(artists)

Of course we will also need this for albums, tracks, album_tracks...

@OzGav
Copy link
Contributor Author

OzGav commented Oct 14, 2025

@MarvinSchenkel Yes I was off on a tangent for a second as I'm doing a million things here. I havent had a chance to test it but I pushed a fix (hopefully!)

Comment on lines +583 to +619
# Get the track
track = await self.mass.music.tracks.get_library_item(item_id)

# Get provider mapping
provider_instance, prov_item_id = await self.mass.music.tracks.get_provider_mapping(
track
)

# Get the provider
prov = self.mass.get_provider(provider_instance)
if not prov or not isinstance(prov, MusicProvider):
raise ProviderUnavailableError(f"Provider {provider_instance} not available")

# Get stream details
streamdetails = await prov.get_stream_details(prov_item_id, MediaType.TRACK)

# Get the absolute path from the FileSystemItem
if hasattr(streamdetails.data, "absolute_path"):
file_path = streamdetails.data.absolute_path
else:
# Fallback for non-filesystem providers
raise UnsupportedFeaturedException(
"Only local files are supported for DLNA streaming"
)

self.logger.debug("Serving file: %s", file_path)

# Serve the file
return cast(
"web.Response", web.FileResponse(path=file_path, headers={"Accept-Ranges": "bytes"})
)

except MediaNotFoundError:
return web.Response(status=404, text="Track not found")
except Exception:
self.logger.exception("Error streaming track")
return web.Response(status=500, text="Internal server error")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a streams controller to handle this. This may not be handled this way directly from a provider

Comment on lines +659 to +715
async def _get_children(
self, parent_id: str, starting_index: int, requested_count: int
) -> tuple[str, int, int]:
"""Get children of a container."""
limit = requested_count if requested_count > 0 else 500
offset = starting_index

if parent_id == ROOT_ID:
containers = [
self._create_artists_root_container(),
self._create_albums_root_container(),
self._create_tracks_root_container(),
]
didl_xml = self._wrap_didl_items(containers)
return didl_xml, len(containers), len(containers)

if parent_id == ARTISTS_CONTAINER_ID:
artists, total = await self._get_filesystem_artists(limit, offset)
artist_items = [self._create_artist_container(artist) for artist in artists]
didl_xml = self._wrap_didl_items(artist_items)
return didl_xml, len(artist_items), total

if parent_id == ALBUMS_CONTAINER_ID:
albums, total = await self._get_filesystem_albums(limit, offset)
album_items = [
self._create_album_container(album, ALBUMS_CONTAINER_ID) for album in albums
]
didl_xml = self._wrap_didl_items(album_items)
return didl_xml, len(album_items), total

if parent_id == TRACKS_CONTAINER_ID:
tracks, total = await self._get_filesystem_tracks(limit, offset)
track_items = [await self._create_track_item(track) for track in tracks]
didl_xml = self._wrap_didl_items(track_items)
return didl_xml, len(track_items), total

if parent_id.startswith("artist_"):
artist_id = parent_id[7:]
albums = await self._get_filesystem_albums_for_artist(artist_id)
paginated_albums = (
list(albums)[offset : offset + limit] if limit > 0 else list(albums)[offset:]
)
album_items = [self._create_album_container(album) for album in paginated_albums]
didl_xml = self._wrap_didl_items(album_items)
return didl_xml, len(album_items), len(albums)

if parent_id.startswith("album_"):
album_id = parent_id[6:]
tracks = await self._get_filesystem_tracks_for_album(album_id)
paginated_tracks = (
list(tracks)[offset : offset + limit] if limit > 0 else list(tracks)[offset:]
)
track_items = [await self._create_track_item(track) for track in paginated_tracks]
didl_xml = self._wrap_didl_items(track_items)
return didl_xml, len(track_items), len(tracks)

return self._create_empty_didl(), 0, 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we not traversing the browse endpoint here or the libraries ?
This is making some very strange assumptions for local filesystems etc.


if parent_id == TRACKS_CONTAINER_ID:
tracks, total = await self._get_filesystem_tracks(limit, offset)
track_items = [await self._create_track_item(track) for track in tracks]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to be horrible slow and its completely unneeded to do this this way


if prov and isinstance(prov, MusicProvider):
try:
streamdetails = await prov.get_stream_details(prov_item_id, MediaType.TRACK)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are fetching the streamdetails for each track in a listing!
That is going to cause MASSIVE issues. This should really be dropped

@marcelveldt marcelveldt marked this pull request as draft October 14, 2025 16:14
@marcelveldt
Copy link
Member

This PR has major architectural challenges for which I think we currently do not have the capacity at all. We have about 80 open user issues reports which all have a higher priority than adding this niche feature to our codebase.

Marked as draft and we will revisit this when capacity is in a better place.
For now I think the vibe coding energy can better be spent at solving small user requests and bugfixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants