diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76e35efeb..b2e51ff0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,7 +76,7 @@ jobs: path: dist/ - name: Publish release to PyPI if: ${{ github.event.release.prerelease == false }} - uses: pypa/gh-action-pypi-publish@v1.12.2 + uses: pypa/gh-action-pypi-publish@v1.12.3 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} diff --git a/music_assistant/controllers/metadata.py b/music_assistant/controllers/metadata.py index 29b433c67..19b4259c9 100644 --- a/music_assistant/controllers/metadata.py +++ b/music_assistant/controllers/metadata.py @@ -123,7 +123,7 @@ def __init__(self, *args, **kwargs) -> None: "Music Assistant's core controller which handles all metadata for music." ) self.manifest.icon = "book-information-variant" - self._lookup_jobs: MetadataLookupQueue = MetadataLookupQueue() + self._lookup_jobs: MetadataLookupQueue = MetadataLookupQueue(100) self._lookup_task: asyncio.Task | None = None self._throttler = Throttler(1, 30) self._missing_metadata_scan_task: asyncio.Task | None = None @@ -796,7 +796,7 @@ async def _scan_missing_metadata(self) -> None: class MetadataLookupQueue(asyncio.Queue): """Representation of a queue for metadata lookups.""" - def _init(self, maxlen: int = 100): + def _init(self, maxlen: int): self._queue: collections.deque[str] = collections.deque(maxlen=maxlen) def _put(self, item: str) -> None: diff --git a/music_assistant/providers/opensubsonic/__init__.py b/music_assistant/providers/opensubsonic/__init__.py index 47e05ac5b..e1d267447 100644 --- a/music_assistant/providers/opensubsonic/__init__.py +++ b/music_assistant/providers/opensubsonic/__init__.py @@ -13,6 +13,7 @@ CONF_BASE_URL, CONF_ENABLE_LEGACY_AUTH, CONF_ENABLE_PODCASTS, + CONF_OVERRIDE_OFFSET, OpenSonicProvider, ) @@ -90,4 +91,13 @@ async def get_config_entries( description='Enable OpenSubsonic "legacy" auth support', default_value=False, ), + ConfigEntry( + key=CONF_OVERRIDE_OFFSET, + type=ConfigEntryType.BOOLEAN, + label="Force player provider seek", + required=True, + description="Some Subsonic implementations advertise that they support seeking when " + "they do not always. If seeking does not work for you, enable this.", + default_value=False, + ), ) diff --git a/music_assistant/providers/opensubsonic/sonic_provider.py b/music_assistant/providers/opensubsonic/sonic_provider.py index 28c877aae..65e49a681 100644 --- a/music_assistant/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/providers/opensubsonic/sonic_provider.py @@ -56,6 +56,7 @@ CONF_BASE_URL = "baseURL" CONF_ENABLE_PODCASTS = "enable_podcasts" CONF_ENABLE_LEGACY_AUTH = "enable_legacy_auth" +CONF_OVERRIDE_OFFSET = "override_transcode_offest" UNKNOWN_ARTIST_ID = "fake_artist_unknown" @@ -71,6 +72,7 @@ class OpenSonicProvider(MusicProvider): _conn: SonicConnection = None _enable_podcasts: bool = True _seek_support: bool = False + _ignore_offset: bool = False async def handle_async_init(self) -> None: """Set up the music provider and test the connection.""" @@ -101,11 +103,12 @@ async def handle_async_init(self) -> None: ) raise LoginFailed(msg) from e self._enable_podcasts = self.config.get_value(CONF_ENABLE_PODCASTS) + self._ignore_offset = self.config.get_value(CONF_OVERRIDE_OFFSET) try: ret = await self._run_async(self._conn.getOpenSubsonicExtensions) extensions = ret["openSubsonicExtensions"] for entry in extensions: - if entry["name"] == "transcodeOffset": + if entry["name"] == "transcodeOffset" and not self._ignore_offset: self._seek_support = True break except OSError: @@ -708,11 +711,14 @@ async def get_stream_details( ) async def _report_playback_started(self, item_id: str) -> None: + self.logger.debug("scrobble for now playing called for %s", item_id) await self._run_async(self._conn.scrobble, sid=item_id, submission=False) async def on_streamed(self, streamdetails: StreamDetails, seconds_streamed: int) -> None: """Handle callback when an item completed streaming.""" + self.logger.debug("on_streamed called for %s", streamdetails.item_id) if seconds_streamed >= streamdetails.duration / 2: + self.logger.debug("scrobble for listen count called for %s", streamdetails.item_id) await self._run_async(self._conn.scrobble, sid=streamdetails.item_id, submission=True) async def get_audio_stream( diff --git a/music_assistant/providers/siriusxm/__init__.py b/music_assistant/providers/siriusxm/__init__.py index f32b5fbda..367246425 100644 --- a/music_assistant/providers/siriusxm/__init__.py +++ b/music_assistant/providers/siriusxm/__init__.py @@ -215,6 +215,15 @@ async def get_stream_details( self, item_id: str, media_type: MediaType = MediaType.RADIO ) -> StreamDetails: """Get streamdetails for a track/radio.""" + # There's a chance that the SiriusXM auth session has expired + # by the time the user clicks to play a station. The sxm-client + # will attempt to reauthenticate automatically, but this causes + # a delay in streaming, and ffmpeg raises a TimeoutError. + # To prevent this, we're going to explicitly authenticate with + # SiriusXM proactively when a station has been chosen to avoid + # this. + await self._client.authenticate() + hls_path = f"http://{self._base_url}/{item_id}.m3u8" # Keep a reference to the current `StreamDetails` object so that we can diff --git a/music_assistant/providers/soundcloud/__init__.py b/music_assistant/providers/soundcloud/__init__.py index f74e88e72..a2a792287 100644 --- a/music_assistant/providers/soundcloud/__init__.py +++ b/music_assistant/providers/soundcloud/__init__.py @@ -438,7 +438,7 @@ async def _parse_track(self, track_obj: dict, playlist_position: int = 0) -> Tra if track_obj.get("description"): track.metadata.description = track_obj["description"] if track_obj.get("genre"): - track.metadata.genres = track_obj["genre"] + track.metadata.genres = [track_obj["genre"]] if track_obj.get("tag_list"): track.metadata.style = track_obj["tag_list"] return track diff --git a/pyproject.toml b/pyproject.toml index 8649eb0a1..2183eefea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "unidecode==1.3.8", "xmltodict==0.14.2", "shortuuid==1.0.13", - "zeroconf==0.136.0", + "zeroconf==0.136.2", ] description = "Music Assistant" license = {text = "Apache-2.0"} diff --git a/requirements_all.txt b/requirements_all.txt index 044d173bc..e4b40c4b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,4 +47,4 @@ unidecode==1.3.8 xmltodict==0.14.2 yt-dlp==2024.10.7 ytmusicapi==1.8.2 -zeroconf==0.136.0 +zeroconf==0.136.2 diff --git a/tests/providers/jellyfin/__snapshots__/test_parsers.ambr b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr index 69fe8a106..439a0f38a 100644 --- a/tests/providers/jellyfin/__snapshots__/test_parsers.ambr +++ b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr @@ -31,7 +31,6 @@ 'item_id': '70b7288088b42d318f75dbcc41fd0091', 'media_type': 'album', 'metadata': dict({ - 'chapters': None, 'copyright': None, 'description': None, 'explicit': None, @@ -45,6 +44,7 @@ }), ]), 'label': None, + 'languages': None, 'last_refresh': None, 'links': None, 'lyrics': None, @@ -52,6 +52,7 @@ 'performers': None, 'popularity': None, 'preview': None, + 'release_date': None, 'review': None, 'style': None, }), @@ -114,7 +115,6 @@ 'item_id': '32ed6a0091733dcff57eae67010f3d4b', 'media_type': 'album', 'metadata': dict({ - 'chapters': None, 'copyright': None, 'description': None, 'explicit': None, @@ -128,6 +128,7 @@ }), ]), 'label': None, + 'languages': None, 'last_refresh': None, 'links': None, 'lyrics': None, @@ -135,6 +136,7 @@ 'performers': None, 'popularity': None, 'preview': None, + 'release_date': None, 'review': None, 'style': None, }), @@ -189,7 +191,6 @@ 'item_id': '7c8d0bd55291c7fc0451d17ebef30017', 'media_type': 'album', 'metadata': dict({ - 'chapters': None, 'copyright': None, 'description': None, 'explicit': None, @@ -197,6 +198,7 @@ 'images': list([ ]), 'label': None, + 'languages': None, 'last_refresh': None, 'links': None, 'lyrics': None, @@ -204,6 +206,7 @@ 'performers': None, 'popularity': None, 'preview': None, + 'release_date': None, 'review': None, 'style': None, }), @@ -246,7 +249,6 @@ 'item_id': 'dd954bbf54398e247d803186d3585b79', 'media_type': 'artist', 'metadata': dict({ - 'chapters': None, 'copyright': None, 'description': None, 'explicit': None, @@ -272,6 +274,7 @@ }), ]), 'label': None, + 'languages': None, 'last_refresh': None, 'links': None, 'lyrics': None, @@ -279,6 +282,7 @@ 'performers': None, 'popularity': None, 'preview': None, + 'release_date': None, 'review': None, 'style': None, }), @@ -346,7 +350,6 @@ 'item_id': 'b5319fb11cde39fca2023184fcfa9862', 'media_type': 'track', 'metadata': dict({ - 'chapters': None, 'copyright': None, 'description': None, 'explicit': None, @@ -354,6 +357,7 @@ 'images': list([ ]), 'label': None, + 'languages': None, 'last_refresh': None, 'links': None, 'lyrics': None, @@ -361,6 +365,7 @@ 'performers': None, 'popularity': None, 'preview': None, + 'release_date': None, 'review': None, 'style': None, }), @@ -417,7 +422,6 @@ 'item_id': '54918f75ee8f6c8b8dc5efd680644f29', 'media_type': 'track', 'metadata': dict({ - 'chapters': None, 'copyright': None, 'description': None, 'explicit': None, @@ -431,6 +435,7 @@ }), ]), 'label': None, + 'languages': None, 'last_refresh': None, 'links': None, 'lyrics': None, @@ -438,6 +443,7 @@ 'performers': None, 'popularity': None, 'preview': None, + 'release_date': None, 'review': None, 'style': None, }), @@ -523,7 +529,6 @@ 'item_id': 'fb12a77f49616a9fc56a6fab2b94174c', 'media_type': 'track', 'metadata': dict({ - 'chapters': None, 'copyright': None, 'description': None, 'explicit': None, @@ -537,6 +542,7 @@ }), ]), 'label': None, + 'languages': None, 'last_refresh': None, 'links': None, 'lyrics': None, @@ -544,6 +550,7 @@ 'performers': None, 'popularity': None, 'preview': None, + 'release_date': None, 'review': None, 'style': None, }),