diff --git a/README.md b/README.md index 66dc738da..5a924a57e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Music Assistant is a free, opensource Media library manager that connects to you Documentation https://music-assistant.io +Beta Documentation https://beta.music-assistant.io + For issues, please go to [the issue tracker](https://github.com/music-assistant/support/issues). For feature requests, please see [feature requests](https://github.com/music-assistant/support/discussions/categories/feature-requests-and-ideas). diff --git a/music_assistant/helpers/playlists.py b/music_assistant/helpers/playlists.py index f1cbbfd3a..3c1244568 100644 --- a/music_assistant/helpers/playlists.py +++ b/music_assistant/helpers/playlists.py @@ -169,7 +169,7 @@ async def fetch_playlist( ) or "#EXT-X-STREAM-INF:" in playlist_data: raise IsHLSPlaylist - if url.endswith((".m3u", ".m3u8")): + if urlparse(url).path.endswith((".m3u", ".m3u8")): playlist = parse_m3u(playlist_data) else: playlist = parse_pls(playlist_data) diff --git a/music_assistant/providers/airplay/__init__.py b/music_assistant/providers/airplay/__init__.py index 83081ea52..ee3282cbf 100644 --- a/music_assistant/providers/airplay/__init__.py +++ b/music_assistant/providers/airplay/__init__.py @@ -8,7 +8,7 @@ from music_assistant_models.enums import ConfigEntryType from music_assistant_models.provider import ProviderManifest -from music_assistant import MusicAssistant +from music_assistant.mass import MusicAssistant from .const import CONF_BIND_INTERFACE from .provider import AirplayProvider @@ -17,7 +17,6 @@ from music_assistant_models.config_entries import ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant from music_assistant.models import ProviderInstanceType diff --git a/music_assistant/providers/airplay/helpers.py b/music_assistant/providers/airplay/helpers.py index ddb6c63a8..4ecb594b4 100644 --- a/music_assistant/providers/airplay/helpers.py +++ b/music_assistant/providers/airplay/helpers.py @@ -30,8 +30,6 @@ def get_model_info(info: AsyncServiceInfo) -> tuple[str, str]: return (manufacturer, model) # try parse from am property if am_property := info.decoded_properties.get("am"): - if isinstance(am_property, bytes): - am_property = am_property.decode("utf-8") model = am_property if not model: diff --git a/music_assistant/providers/airplay/provider.py b/music_assistant/providers/airplay/provider.py index d44963031..3549b4898 100644 --- a/music_assistant/providers/airplay/provider.py +++ b/music_assistant/providers/airplay/provider.py @@ -8,7 +8,7 @@ import socket import time from random import randrange -from typing import TYPE_CHECKING +from typing import cast from music_assistant_models.config_entries import ConfigEntry from music_assistant_models.enums import ( @@ -36,12 +36,13 @@ CONF_ENTRY_SYNC_ADJUST, create_sample_rates_config_entry, ) -from music_assistant.helpers.audio import get_ffmpeg_stream from music_assistant.helpers.datetime import utc +from music_assistant.helpers.ffmpeg import get_ffmpeg_stream from music_assistant.helpers.process import check_output from music_assistant.helpers.util import TaskManager, get_ip_pton, lock, select_free_port from music_assistant.models.player_provider import PlayerProvider from music_assistant.providers.airplay.raop import RaopStreamSession +from music_assistant.providers.player_group import PlayerGroupProvider from .const import ( AIRPLAY_FLOW_PCM_FORMAT, @@ -61,10 +62,6 @@ ) from .player import AirPlayPlayer -if TYPE_CHECKING: - from music_assistant.providers.player_group import PlayerGroupProvider - - PLAYER_CONFIG_ENTRIES = ( CONF_ENTRY_FLOW_MODE_ENFORCED, CONF_ENTRY_CROSSFADE, @@ -138,15 +135,15 @@ class AirplayProvider(PlayerProvider): """Player provider for Airplay based players.""" - cliraop_bin: str | None = None + cliraop_bin: str | None _players: dict[str, AirPlayPlayer] - _dacp_server: asyncio.Server = None - _dacp_info: AsyncServiceInfo = None + _dacp_server: asyncio.Server + _dacp_info: AsyncServiceInfo @property def supported_features(self) -> set[ProviderFeature]: """Return the features supported by this Provider.""" - return (ProviderFeature.SYNC_PLAYERS,) + return {ProviderFeature.SYNC_PLAYERS} async def handle_async_init(self) -> None: """Handle async initialization of the provider.""" @@ -179,10 +176,12 @@ async def on_mdns_service_state_change( self, name: str, state_change: ServiceStateChange, info: AsyncServiceInfo | None ) -> None: """Handle MDNS service state callback.""" + if not info: + return if "@" in name: raw_id, display_name = name.split(".")[0].split("@", 1) - elif "deviceid" in info.decoded_properties: - raw_id = info.decoded_properties["deviceid"].replace(":", "") + elif deviceid := info.decoded_properties.get("deviceid"): + raw_id = deviceid.replace(":", "") display_name = info.name.split(".")[0] else: return @@ -263,6 +262,8 @@ async def cmd_pause(self, player_id: str) -> None: - player_id: player_id of the player to handle the command. """ player = self.mass.players.get(player_id) + if not player: + return if player.group_childs: # pause is not supported while synced, use stop instead self.logger.debug("Player is synced, using STOP instead of PAUSE") @@ -279,6 +280,8 @@ async def play_media( ) -> None: """Handle PLAY MEDIA on given player.""" player = self.mass.players.get(player_id) + if not player: + return # set the active source for the player to the media queue # this accounts for syncgroups and linked players (e.g. sonos) player.active_source = media.queue_id @@ -300,7 +303,7 @@ async def play_media( ) elif media.queue_id and media.queue_id.startswith("ugp_"): # special case: UGP stream - ugp_provider: PlayerGroupProvider = self.mass.get_provider("player_group") + ugp_provider = cast(PlayerGroupProvider, self.mass.get_provider("player_group")) ugp_stream = ugp_provider.ugp_streams[media.queue_id] input_format = ugp_stream.output_format audio_source = ugp_stream.subscribe() @@ -338,6 +341,8 @@ async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: if airplay_player.raop_stream and airplay_player.raop_stream.running: await airplay_player.raop_stream.send_cli_command(f"VOLUME={volume_level}\n") mass_player = self.mass.players.get(player_id) + if not mass_player: + return mass_player.volume_level = volume_level mass_player.volume_muted = volume_level == 0 self.mass.players.update(player_id) @@ -404,22 +409,24 @@ async def cmd_ungroup(self, player_id: str) -> None: - player_id: player_id of the player to handle the command. """ mass_player = self.mass.players.get(player_id, raise_unavailable=True) - if not mass_player.synced_to: + if not mass_player or not mass_player.synced_to: return ap_player = self._players[player_id] if ap_player.raop_stream and ap_player.raop_stream.running: await ap_player.raop_stream.session.remove_client(ap_player) group_leader = self.mass.players.get(mass_player.synced_to, raise_unavailable=True) + assert group_leader if player_id in group_leader.group_childs: group_leader.group_childs.remove(player_id) mass_player.synced_to = None airplay_player = self._players.get(player_id) - await airplay_player.cmd_stop() + if airplay_player: + await airplay_player.cmd_stop() # make sure that the player manager gets an update self.mass.players.update(mass_player.player_id, skip_forward=True) self.mass.players.update(group_leader.player_id, skip_forward=True) - async def _getcliraop_binary(self): + async def _getcliraop_binary(self) -> str: """Find the correct raop/airplay binary belonging to the platform.""" # ruff: noqa: SIM102 if self.cliraop_bin is not None: @@ -435,7 +442,8 @@ async def check_binary(cliraop_path: str) -> str | None: self.cliraop_bin = cliraop_path return cliraop_path except OSError: - return None + pass + return None base_path = os.path.join(os.path.dirname(__file__), "bin") system = platform.system().lower().replace("darwin", "macos") @@ -452,6 +460,7 @@ async def check_binary(cliraop_path: str) -> str | None: def _get_sync_clients(self, player_id: str) -> list[AirPlayPlayer]: """Get all sync clients for a player.""" mass_player = self.mass.players.get(player_id, True) + assert mass_player sync_clients: list[AirPlayPlayer] = [] # we need to return the player itself too group_child_ids = {player_id} @@ -541,15 +550,15 @@ async def _handle_dacp_request( # noqa: PLR0915 else: headers_raw = request body = "" - headers_raw = headers_raw.split("\r\n") + headers_split = headers_raw.split("\r\n") headers = {} - for line in headers_raw[1:]: + for line in headers_split[1:]: if ":" not in line: continue x, y = line.split(":", 1) headers[x.strip()] = y.strip() active_remote = headers.get("Active-Remote") - _, path, _ = headers_raw[0].split(" ") + _, path, _ = headers_split[0].split(" ") airplay_player = next( ( x @@ -570,6 +579,8 @@ async def _handle_dacp_request( # noqa: PLR0915 player_id = airplay_player.player_id mass_player = self.mass.players.get(player_id) + if not mass_player: + return active_queue = self.mass.player_queues.get_active_queue(player_id) if path == "/ctrl-int/1/nextitem": self.mass.create_task(self.mass.player_queues.next(active_queue.queue_id)) @@ -590,10 +601,10 @@ async def _handle_dacp_request( # noqa: PLR0915 self.mass.create_task(self.mass.players.cmd_volume_down(player_id)) elif path == "/ctrl-int/1/shuffle_songs": queue = self.mass.player_queues.get(player_id) - self.mass.loop.call_soon( - self.mass.player_queues.set_shuffle( - active_queue.queue_id, not queue.shuffle_enabled - ) + if not queue: + return + self.mass.player_queues.set_shuffle( + active_queue.queue_id, not queue.shuffle_enabled ) elif path in ("/ctrl-int/1/pause", "/ctrl-int/1/discrete-pause"): # sometimes this request is sent by a device as confirmation of a play command @@ -650,11 +661,13 @@ async def _handle_dacp_request( # noqa: PLR0915 finally: writer.close() - async def monitor_prevent_playback(self, player_id: str): + async def monitor_prevent_playback(self, player_id: str) -> None: """Monitor the prevent playback state of an airplay player.""" count = 0 if not (airplay_player := self._players.get(player_id)): return + if not airplay_player.raop_stream: + return prev_active_remote_id = airplay_player.raop_stream.active_remote_id while count < 40: count += 1 diff --git a/music_assistant/providers/airplay/raop.py b/music_assistant/providers/airplay/raop.py index 98841c263..27c6062aa 100644 --- a/music_assistant/providers/airplay/raop.py +++ b/music_assistant/providers/airplay/raop.py @@ -54,7 +54,7 @@ def __init__( self.input_format = input_format self._sync_clients = sync_clients self._audio_source = audio_source - self._audio_source_task: asyncio.Task | None = None + self._audio_source_task: asyncio.Task[None] | None = None self._stopped: bool = False self._lock = asyncio.Lock() @@ -75,14 +75,18 @@ async def audio_streamer() -> None: return async with self._lock: await asyncio.gather( - *[x.raop_stream.write_chunk(chunk) for x in self._sync_clients], + *[ + x.raop_stream.write_chunk(chunk) + for x in self._sync_clients + if x.raop_stream + ], return_exceptions=True, ) # entire stream consumed: send EOF generator_exhausted = True async with self._lock: await asyncio.gather( - *[x.raop_stream.write_eof() for x in self._sync_clients], + *[x.raop_stream.write_eof() for x in self._sync_clients if x.raop_stream], return_exceptions=True, ) finally: @@ -90,12 +94,17 @@ async def audio_streamer() -> None: await close_async_generator(self._audio_source) # get current ntp and start RaopStream per player + assert self.prov.cliraop_bin _, stdout = await check_output(self.prov.cliraop_bin, "-ntp") start_ntp = int(stdout.strip()) wait_start = 1500 + (250 * len(self._sync_clients)) async with self._lock: await asyncio.gather( - *[x.raop_stream.start(start_ntp, wait_start) for x in self._sync_clients], + *[ + x.raop_stream.start(start_ntp, wait_start) + for x in self._sync_clients + if x.raop_stream + ], return_exceptions=True, ) self._audio_source_task = asyncio.create_task(audio_streamer()) @@ -116,6 +125,7 @@ async def remove_client(self, airplay_player: AirPlayPlayer) -> None: """Remove a sync client from the session.""" if airplay_player not in self._sync_clients: return + assert airplay_player.raop_stream assert airplay_player.raop_stream.session == self async with self._lock: self._sync_clients.remove(airplay_player) @@ -154,7 +164,7 @@ def __init__( # with the named pipe used to send audio self.active_remote_id: str = str(randint(1000, 8000)) self.prevent_playback: bool = False - self._log_reader_task: asyncio.Task | None = None + self._log_reader_task: asyncio.Task[None] | asyncio.Future[None] | None = None self._cliraop_proc: AsyncProcess | None = None self._ffmpeg_proc: AsyncProcess | None = None self._started = asyncio.Event() @@ -170,6 +180,8 @@ async def start(self, start_ntp: int, wait_start: int = 1000) -> None: extra_args = [] player_id = self.airplay_player.player_id mass_player = self.mass.players.get(player_id) + if not mass_player: + return bind_ip = await self.mass.config.get_provider_config_value( self.prov.instance_id, CONF_BIND_INTERFACE ) @@ -240,10 +252,12 @@ async def start(self, start_ntp: int, wait_start: int = 1000) -> None: self._started.set() self._log_reader_task = self.mass.create_task(self._log_watcher()) - async def stop(self): + async def stop(self) -> None: """Stop playback and cleanup.""" if self._stopped: return + if not self._cliraop_proc: + return if self._cliraop_proc.proc and not self._cliraop_proc.closed: await self.send_cli_command("ACTION=STOP") self._stopped = True # set after send_cli command! @@ -259,6 +273,7 @@ async def write_chunk(self, chunk: bytes) -> None: if self._stopped: return await self._started.wait() + assert self._ffmpeg_proc await self._ffmpeg_proc.write(chunk) async def write_eof(self) -> None: @@ -266,6 +281,7 @@ async def write_eof(self) -> None: if self._stopped: return await self._started.wait() + assert self._ffmpeg_proc await self._ffmpeg_proc.write_eof() async def send_cli_command(self, command: str) -> None: @@ -277,7 +293,7 @@ async def send_cli_command(self, command: str) -> None: if not command.endswith("\n"): command += "\n" - def send_data(): + def send_data() -> None: with suppress(BrokenPipeError), open(named_pipe, "w") as f: f.write(command) @@ -290,11 +306,15 @@ async def _log_watcher(self) -> None: """Monitor stderr for the running CLIRaop process.""" airplay_player = self.airplay_player mass_player = self.mass.players.get(airplay_player.player_id) + if not mass_player: + return queue = self.mass.player_queues.get_active_queue(mass_player.active_source) logger = airplay_player.logger lost_packets = 0 prev_metadata_checksum: str = "" prev_progress_report: float = 0 + if not self._cliraop_proc: + return async for line in self._cliraop_proc.iter_stderr(): if "elapsed milliseconds:" in line: # this is received more or less every second while playing diff --git a/music_assistant/providers/builtin/__init__.py b/music_assistant/providers/builtin/__init__.py index 25aa0c622..1786e35e2 100644 --- a/music_assistant/providers/builtin/__init__.py +++ b/music_assistant/providers/builtin/__init__.py @@ -48,7 +48,7 @@ from music_assistant_models.config_entries import ConfigValueType, ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType diff --git a/music_assistant/providers/filesystem_local/__init__.py b/music_assistant/providers/filesystem_local/__init__.py index fa746715f..51691abee 100644 --- a/music_assistant/providers/filesystem_local/__init__.py +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -74,7 +74,7 @@ from music_assistant_models.config_entries import ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType CONF_MISSING_ALBUM_ARTIST_ACTION = "missing_album_artist_action" diff --git a/music_assistant/providers/filesystem_local/helpers.py b/music_assistant/providers/filesystem_local/helpers.py index e7a66201d..a42416f62 100644 --- a/music_assistant/providers/filesystem_local/helpers.py +++ b/music_assistant/providers/filesystem_local/helpers.py @@ -61,7 +61,7 @@ def relative_parent_path(self) -> str: return os.path.dirname(self.relative_path) @classmethod - def from_dir_entry(cls, entry: os.DirEntry, base_path: str) -> FileSystemItem: + def from_dir_entry(cls, entry: os.DirEntry[str], base_path: str) -> FileSystemItem: """Create FileSystemItem from os.DirEntry. NOT Async friendly.""" if entry.is_dir(follow_symlinks=False): return cls( diff --git a/music_assistant/providers/filesystem_smb/__init__.py b/music_assistant/providers/filesystem_smb/__init__.py index b29b5b712..3d2c9cfc1 100644 --- a/music_assistant/providers/filesystem_smb/__init__.py +++ b/music_assistant/providers/filesystem_smb/__init__.py @@ -24,7 +24,7 @@ from music_assistant_models.config_entries import ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType CONF_HOST = "host" diff --git a/music_assistant/providers/fully_kiosk/__init__.py b/music_assistant/providers/fully_kiosk/__init__.py index bc49df356..6f7b2c296 100644 --- a/music_assistant/providers/fully_kiosk/__init__.py +++ b/music_assistant/providers/fully_kiosk/__init__.py @@ -30,7 +30,7 @@ from music_assistant_models.config_entries import ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType AUDIOMANAGER_STREAM_MUSIC = 3 diff --git a/music_assistant/providers/jellyfin/__init__.py b/music_assistant/providers/jellyfin/__init__.py index 8ccdd425c..3390ee336 100644 --- a/music_assistant/providers/jellyfin/__init__.py +++ b/music_assistant/providers/jellyfin/__init__.py @@ -10,8 +10,9 @@ from typing import TYPE_CHECKING from aiojellyfin import MediaLibrary as JellyMediaLibrary -from aiojellyfin import NotFound, SessionConfiguration, authenticate_by_name +from aiojellyfin import NotFound, authenticate_by_name from aiojellyfin import Track as JellyTrack +from aiojellyfin.session import SessionConfiguration from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig from music_assistant_models.enums import ( ConfigEntryType, @@ -32,8 +33,8 @@ ) from music_assistant_models.streamdetails import StreamDetails -from music_assistant import MusicAssistant from music_assistant.constants import UNKNOWN_ARTIST_ID_MBID +from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType from music_assistant.models.music_provider import MusicProvider from music_assistant.providers.jellyfin.parsers import ( @@ -409,23 +410,21 @@ async def get_playlist(self, prov_playlist_id: str) -> Playlist: async def get_playlist_tracks(self, prov_playlist_id: str, page: int = 0) -> list[Track]: """Get playlist tracks.""" result: list[Track] = [] - if page > 0: - # paging not supported, we always return the whole list at once - return [] - # TODO: Does Jellyfin support paging here? playlist_items = ( - await self._client.tracks.parent(prov_playlist_id) + await self._client.tracks.in_playlist(prov_playlist_id) .enable_userdata() .fields(*TRACK_FIELDS) + .limit(100) + .start_index(page * 100) .request() ) for index, jellyfin_track in enumerate(playlist_items["Items"], 1): + pos = (page * 100) + index try: if track := parse_track( self.logger, self.instance_id, self._client, jellyfin_track ): - if not track.position: - track.position = index + track.position = pos result.append(track) except (KeyError, ValueError) as err: self.logger.error( diff --git a/music_assistant/providers/jellyfin/manifest.json b/music_assistant/providers/jellyfin/manifest.json index cf9cca560..af85a57b7 100644 --- a/music_assistant/providers/jellyfin/manifest.json +++ b/music_assistant/providers/jellyfin/manifest.json @@ -4,7 +4,7 @@ "name": "Jellyfin Media Server Library", "description": "Support for the Jellyfin streaming provider in Music Assistant.", "codeowners": ["@lokiberra", "@Jc2k"], - "requirements": ["aiojellyfin==0.10.1"], + "requirements": ["aiojellyfin==0.11.2"], "documentation": "https://music-assistant.io/music-providers/jellyfin/", "multi_instance": true } diff --git a/music_assistant/providers/player_group/__init__.py b/music_assistant/providers/player_group/__init__.py index bea0788bb..9d89d133d 100644 --- a/music_assistant/providers/player_group/__init__.py +++ b/music_assistant/providers/player_group/__init__.py @@ -36,7 +36,7 @@ ProviderUnavailableError, UnsupportedFeaturedException, ) -from music_assistant_models.media_items import AudioFormat +from music_assistant_models.media_items import AudioFormat, UniqueList from music_assistant_models.player import DeviceInfo, Player, PlayerMedia from music_assistant.constants import ( @@ -711,7 +711,7 @@ async def _register_group_player( needs_poll=True, poll_interval=30, can_group_with=can_group_with, - group_childs=set(members), + group_childs=UniqueList(members), ) await self.mass.players.register_or_update(player) diff --git a/music_assistant/providers/plex/__init__.py b/music_assistant/providers/plex/__init__.py index f6967b71e..f5ccc32fe 100644 --- a/music_assistant/providers/plex/__init__.py +++ b/music_assistant/providers/plex/__init__.py @@ -69,7 +69,7 @@ from plexapi.media import Media as PlexMedia from plexapi.media import MediaPart as PlexMediaPart - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType CONF_ACTION_AUTH_MYPLEX = "auth_myplex" diff --git a/music_assistant/providers/plex/helpers.py b/music_assistant/providers/plex/helpers.py index 0c303bcc9..98850835b 100644 --- a/music_assistant/providers/plex/helpers.py +++ b/music_assistant/providers/plex/helpers.py @@ -12,7 +12,7 @@ from plexapi.server import PlexServer if TYPE_CHECKING: - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant async def get_libraries( diff --git a/music_assistant/providers/radiobrowser/__init__.py b/music_assistant/providers/radiobrowser/__init__.py index 318c0aa24..03962792e 100644 --- a/music_assistant/providers/radiobrowser/__init__.py +++ b/music_assistant/providers/radiobrowser/__init__.py @@ -47,7 +47,7 @@ from music_assistant_models.config_entries import ConfigValueType, ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType CONF_STORED_RADIOS = "stored_radios" diff --git a/music_assistant/providers/test/__init__.py b/music_assistant/providers/test/__init__.py index e04e62985..92f575605 100644 --- a/music_assistant/providers/test/__init__.py +++ b/music_assistant/providers/test/__init__.py @@ -31,18 +31,14 @@ ) from music_assistant_models.streamdetails import StreamDetails -from music_assistant.constants import ( - MASS_LOGO, - SILENCE_FILE_LONG, - VARIOUS_ARTISTS_FANART, -) +from music_assistant.constants import MASS_LOGO, SILENCE_FILE_LONG, VARIOUS_ARTISTS_FANART from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: from music_assistant_models.config_entries import ConfigValueType, ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType diff --git a/music_assistant/providers/theaudiodb/__init__.py b/music_assistant/providers/theaudiodb/__init__.py index cfaca28be..ded4f5598 100644 --- a/music_assistant/providers/theaudiodb/__init__.py +++ b/music_assistant/providers/theaudiodb/__init__.py @@ -35,7 +35,7 @@ from music_assistant_models.config_entries import ConfigValueType, ProviderConfig from music_assistant_models.provider import ProviderManifest - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType SUPPORTED_FEATURES = { diff --git a/music_assistant/providers/tidal/__init__.py b/music_assistant/providers/tidal/__init__.py index 2e0701084..8dac82e16 100644 --- a/music_assistant/providers/tidal/__init__.py +++ b/music_assistant/providers/tidal/__init__.py @@ -82,7 +82,7 @@ from tidalapi.media import Lyrics as TidalLyrics from tidalapi.media import Stream as TidalStream - from music_assistant import MusicAssistant + from music_assistant.mass import MusicAssistant from music_assistant.models import ProviderInstanceType TOKEN_TYPE = "Bearer" diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index aa3c6247b..000000000 --- a/mypy.ini +++ /dev/null @@ -1,24 +0,0 @@ -[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.providers.builtin,music_assistant.providers.filesystem_local,music_assistant.providers.filesystem_smb,music_assistant.providers.fully_kiosk,music_assistant.providers.jellyfin,music_assistant.providers.plex,music_assistant.providers.radiobrowser,music_assistant.providers.test,music_assistant.providers.theaudiodb,music_assistant.providers.tidal diff --git a/pyproject.toml b/pyproject.toml index 47716102d..8f9e5ab61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,10 +92,10 @@ max-statements = 50 [tool.mypy] platform = "linux" -python_version = "3.11" +python_version = "3.12" -# show error messages from unrelated files -follow_imports = "normal" +# set this to normal when we have fixed all exclusions +follow_imports = "silent" # suppress errors about unsatisfied imports ignore_missing_imports = true @@ -117,6 +117,57 @@ warn_redundant_casts = true warn_return_any = true warn_unused_configs = true warn_unused_ignores = true +show_error_codes = true +local_partial_types = true +strict_equality = true +enable_error_code = [ + "ignore-without-code", + "redundant-self", + "truthy-iterable", +] +disable_error_code = [ + "annotation-unchecked", + "import-not-found", + "import-untyped" +] +extra_checks = false +warn_unreachable = true +packages = [ + "tests", + "music_assistant", +] +exclude = [ + '^music_assistant/controllers/.*$', + '^music_assistant/helpers/.*$', + '^music_assistant/models/.*$', + '^music_assistant/mass\.py$', + '^music_assistant/__main__\.py$', + '^music_assistant/providers/_template_music_provider/.*$', + '^music_assistant/providers/_template_player_provider/.*$', + '^music_assistant/providers/apple_music/.*$', + '^music_assistant/providers/bluesound/.*$', + '^music_assistant/providers/chromecast/.*$', + '^music_assistant/providers/deezer/.*$', + '^music_assistant/providers/dlna/.*$', + '^music_assistant/providers/fanarttv/.*$', + '^music_assistant/providers/hass/.*$', + '^music_assistant/providers/hass_players/.*$', + '^music_assistant/providers/ibroadcast/.*$', + '^music_assistant/providers/musicbrainz/.*$', + '^music_assistant/providers/opensubsonic/.*$', + '^music_assistant/providers/player_group/.*$', + '^music_assistant/providers/podcastfeed/.*$', + '^music_assistant/providers/qobuz/.*$', + '^music_assistant/providers/siriusxm/.*$', + '^music_assistant/providers/slimproto/.*$', + '^music_assistant/providers/sonos/.*$', + '^music_assistant/providers/sonos_s1/.*$', + '^music_assistant/providers/soundcloud/.*$', + '^music_assistant/providers/snapcast/.*$', + '^music_assistant/providers/spotify/.*$', + '^music_assistant/providers/tunein/.*$', + '^music_assistant/providers/ytmusic/.*$', +] [tool.ruff.format] # Force Linux/MacOS line endings diff --git a/requirements_all.txt b/requirements_all.txt index c2cde8b66..1455e75e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ Brotli>=1.0.9 aiodns>=3.0.0 aiofiles==24.1.0 aiohttp==3.11.6 -aiojellyfin==0.10.1 +aiojellyfin==0.11.2 aiorun==2024.8.1 aioslimproto==3.1.0 aiosonos==0.1.7 diff --git a/tests/common.py b/tests/common.py index b2be5ddf8..52536b972 100644 --- a/tests/common.py +++ b/tests/common.py @@ -9,7 +9,7 @@ from music_assistant_models.enums import EventType from music_assistant_models.event import MassEvent -from music_assistant import MusicAssistant +from music_assistant.mass import MusicAssistant def _get_fixture_folder(provider: str | None = None) -> pathlib.Path: diff --git a/tests/conftest.py b/tests/conftest.py index de17ce16c..2dc3e1f63 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest -from music_assistant import MusicAssistant +from music_assistant.mass import MusicAssistant @pytest.fixture(name="caplog") diff --git a/tests/core/test_server_base.py b/tests/core/test_server_base.py index d40867872..0bf116da6 100644 --- a/tests/core/test_server_base.py +++ b/tests/core/test_server_base.py @@ -5,7 +5,7 @@ from music_assistant_models.enums import EventType from music_assistant_models.event import MassEvent -from music_assistant import MusicAssistant +from music_assistant.mass import MusicAssistant async def test_start_and_stop_server(mass: MusicAssistant) -> None: diff --git a/tests/core/test_tags.py b/tests/core/test_tags.py index d04a33f3e..663b8eb1d 100644 --- a/tests/core/test_tags.py +++ b/tests/core/test_tags.py @@ -2,6 +2,7 @@ import pathlib +from music_assistant.constants import UNKNOWN_ARTIST from music_assistant.helpers import tags RESOURCES_DIR = pathlib.Path(__file__).parent.parent.resolve().joinpath("fixtures") @@ -67,7 +68,7 @@ async def test_parse_metadata_from_invalid_filename() -> None: assert _tags.title == "test" assert _tags.duration == 1.032 assert _tags.album_artists == () - assert _tags.artists == (tags.UNKNOWN_ARTIST,) + assert _tags.artists == (UNKNOWN_ARTIST,) assert _tags.genres == () assert _tags.musicbrainz_albumartistids == () assert _tags.musicbrainz_artistids == () diff --git a/tests/providers/jellyfin/test_init.py b/tests/providers/jellyfin/test_init.py index 792aa0eec..4db3828b6 100644 --- a/tests/providers/jellyfin/test_init.py +++ b/tests/providers/jellyfin/test_init.py @@ -7,7 +7,7 @@ from aiojellyfin.testing import FixtureBuilder from music_assistant_models.config_entries import ProviderConfig -from music_assistant import MusicAssistant +from music_assistant.mass import MusicAssistant from tests.common import get_fixtures_dir, wait_for_sync_completion diff --git a/tests/providers/jellyfin/test_parsers.py b/tests/providers/jellyfin/test_parsers.py index 7952cae51..dc29cc5ec 100644 --- a/tests/providers/jellyfin/test_parsers.py +++ b/tests/providers/jellyfin/test_parsers.py @@ -7,7 +7,8 @@ import aiofiles import aiohttp import pytest -from aiojellyfin import Artist, Connection, SessionConfiguration +from aiojellyfin import Artist, Connection +from aiojellyfin.session import SessionConfiguration from mashumaro.codecs.json import JSONDecoder from syrupy.assertion import SnapshotAssertion