# types: [text]
# entry: scripts/run-in-env.sh trailing-whitespace-fixer
# stages: [commit, push, manual]
- # - id: mypy
- # name: mypy
- # entry: scripts/run-in-env.sh mypy
- # language: script
- # types: [python]
- # require_serial: true
- # files: ^(music_assistant|pylint)/.+\.py$
+ - id: mypy
+ name: mypy
+ entry: scripts/run-in-env.sh mypy
+ language: script
+ types: [python]
+ require_serial: true
+ pass_filenames: false
- id: gen_requirements_all
name: gen_requirements_all
entry: scripts/run-in-env.sh python3 -m scripts.gen_requirements_all
return ""
@property
- def album(self) -> str:
+ def album(self) -> str | None:
"""Return album tag (as-is) if present."""
return self.tags.get("album")
from collections.abc import AsyncGenerator
from aiojellyfin import MediaLibrary as JellyMediaLibrary
-from aiojellyfin import SessionConfiguration, authenticate_by_name
+from aiojellyfin import NotFound, SessionConfiguration, authenticate_by_name
from aiojellyfin import Track as JellyTrack
from music_assistant.common.models.config_entries import (
async def get_album(self, prov_album_id: str) -> Album:
"""Get full album details by id."""
- if jellyfin_album := await self._client.get_album(prov_album_id):
- return parse_album(self.logger, self.instance_id, self._client, jellyfin_album)
- msg = f"Item {prov_album_id} not found"
- raise MediaNotFoundError(msg)
+ try:
+ album = await self._client.get_album(prov_album_id)
+ except NotFound:
+ raise MediaNotFoundError(f"Item {prov_album_id} not found")
+ return parse_album(self.logger, self.instance_id, self._client, album)
async def get_album_tracks(self, prov_album_id: str) -> list[Track]:
"""Get album tracks for given album id."""
artist.mbid = UNKNOWN_ARTIST_ID_MBID
return artist
- if jellyfin_artist := await self._client.get_artist(prov_artist_id):
- return parse_artist(self.logger, self.instance_id, self._client, jellyfin_artist)
- msg = f"Item {prov_artist_id} not found"
- raise MediaNotFoundError(msg)
+ try:
+ jellyfin_artist = await self._client.get_artist(prov_artist_id)
+ except NotFound:
+ raise MediaNotFoundError(f"Item {prov_artist_id} not found")
+ return parse_artist(self.logger, self.instance_id, self._client, jellyfin_artist)
async def get_track(self, prov_track_id: str) -> Track:
"""Get full track details by id."""
- if jellyfin_track := await self._client.get_track(prov_track_id):
- return parse_track(self.logger, self.instance_id, self._client, jellyfin_track)
- msg = f"Item {prov_track_id} not found"
- raise MediaNotFoundError(msg)
+ try:
+ track = await self._client.get_track(prov_track_id)
+ except NotFound:
+ raise MediaNotFoundError(f"Item {prov_track_id} not found")
+ return parse_track(self.logger, self.instance_id, self._client, track)
async def get_playlist(self, prov_playlist_id: str) -> Playlist:
"""Get full playlist details by id."""
- if jellyfin_playlist := await self._client.get_playlist(prov_playlist_id):
- return parse_playlist(self.instance_id, self._client, jellyfin_playlist)
- msg = f"Item {prov_playlist_id} not found"
- raise MediaNotFoundError(msg)
+ try:
+ playlist = await self._client.get_playlist(prov_playlist_id)
+ except NotFound:
+ raise MediaNotFoundError(f"Item {prov_playlist_id} not found")
+ return parse_playlist(self.instance_id, self._client, playlist)
async def get_playlist_tracks(
self, prov_playlist_id: str, offset: int, limit: int
playlist_items = await self._client.tracks(
jellyfin_playlist[ITEM_KEY_ID], enable_user_data=True, fields=TRACK_FIELDS
)
- if not playlist_items:
- return result
for index, jellyfin_track in enumerate(playlist_items["Items"], 1):
try:
if track := parse_track(
path=url,
)
- async def get_similar_tracks(self, prov_track_id, limit=25) -> list[Track]:
+ async def get_similar_tracks(self, prov_track_id: str, limit: int = 25) -> list[Track]:
"""Retrieve a dynamic list of tracks based on the provided item."""
resp = await self._client.get_similar_tracks(prov_track_id, limit=limit)
return [
"name": "Jellyfin Media Server Library",
"description": "Support for the Jellyfin streaming provider in Music Assistant.",
"codeowners": ["@lokiberra", "@Jc2k"],
- "requirements": ["aiojellyfin==0.9.0"],
+ "requirements": ["aiojellyfin==0.9.1"],
"documentation": "https://music-assistant.io/music-providers/jellyfin/",
"multi_instance": true
}
from aiojellyfin.const import ImageType as JellyImageType
-from music_assistant.common.models.enums import (
- ContentType,
- ImageType,
- MediaType,
-)
+from music_assistant.common.models.enums import ContentType, ImageType, MediaType
from music_assistant.common.models.errors import InvalidDataError
from music_assistant.common.models.media_items import (
Album,
track.disc_number = jellyfin_track.get(ITEM_KEY_PARENT_INDEX_NUM, 0)
track.track_number = jellyfin_track.get("IndexNumber", 0)
- if track.track_number >= 0:
+ if track.track_number is not None and track.track_number >= 0:
track.position = track.track_number
track.metadata.images = _get_artwork(instance_id, client, jellyfin_track)
--- /dev/null
+[mypy]
+python_version = 3.12
+platform = linux
+show_error_codes = true
+follow_imports = silent
+local_partial_types = true
+strict_equality = true
+no_implicit_optional = true
+warn_incomplete_stub = true
+warn_redundant_casts = true
+warn_unused_configs = true
+warn_unused_ignores = true
+enable_error_code = ignore-without-code, redundant-self, truthy-iterable
+disable_error_code = annotation-unchecked, import-not-found, import-untyped
+extra_checks = false
+check_untyped_defs = true
+disallow_incomplete_defs = true
+disallow_subclassing_any = true
+disallow_untyped_calls = true
+disallow_untyped_decorators = true
+disallow_untyped_defs = true
+warn_return_any = true
+warn_unreachable = true
+packages=tests,music_assistant.client,music_assistant.server.providers.jellyfin
aiodns>=3.0.0
aiofiles==24.1.0
aiohttp==3.9.5
-aiojellyfin==0.9.0
+aiojellyfin==0.9.1
aiorun==2024.5.1
aioslimproto==3.0.1
aiosqlite==0.20.0
FILE_1 = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle.mp3"))
-async def test_parse_metadata_from_id3tags():
+async def test_parse_metadata_from_id3tags() -> None:
"""Test parsing of parsing metadata from ID3 tags."""
filename = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle.mp3"))
_tags = await tags.parse_tags(filename)
assert _tags.musicbrainz_releasegroupid == "abcdefg"
assert _tags.musicbrainz_recordingid == "abcdefg"
# test parsing disc/track number
+ _tags.tags["disc"] = ""
+ assert _tags.disc is None
_tags.tags["disc"] = "1"
assert _tags.disc == 1
_tags.tags["disc"] = "1/1"
assert _tags.disc == 1
- _tags.tags["disc"] = ""
- assert _tags.disc is None
# test parsing album year
+ _tags.tags["date"] = "blah"
+ assert _tags.year is None
+ _tags.tags.pop("date", None)
+ assert _tags.year is None
_tags.tags["date"] = "2022"
assert _tags.year == 2022
_tags.tags["date"] = "2022-05-05"
assert _tags.year == 2022
- _tags.tags["date"] = "blah"
- assert _tags.year is None
_tags.tags["date"] = ""
assert _tags.year is None
- _tags.tags.pop("date", None)
- assert _tags.year is None
-async def test_parse_metadata_from_filename():
+async def test_parse_metadata_from_filename() -> None:
"""Test parsing of parsing metadata from filename."""
filename = str(RESOURCES_DIR.joinpath("MyArtist - MyTitle without Tags.mp3"))
_tags = await tags.parse_tags(filename)
assert _tags.musicbrainz_recordingid is None
-async def test_parse_metadata_from_invalid_filename():
+async def test_parse_metadata_from_invalid_filename() -> None:
"""Test parsing of parsing metadata from (invalid) filename."""
filename = str(RESOURCES_DIR.joinpath("test.mp3"))
_tags = await tags.parse_tags(filename)