-
-
Notifications
You must be signed in to change notification settings - Fork 198
Add Dlna server Plugin #2501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Add Dlna server Plugin #2501
Conversation
|
Just had a little play with this. Nice job! A few things I noticed:
Now, it also seems to index media from streaming providers. When opening the 'Artist' folder, it seems to call 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? |
Co-authored-by: Marcel van der Veldt <[email protected]>
… into dlna-server
|
@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 Add a constant on the top: Then inside Of course we will also need this for albums, tracks, album_tracks... |
|
@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!) |
| # 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") |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
|
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. |
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