From 0acafdbdc1fe76d010aebce8bc6cbccd2fae9a05 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 7 May 2024 20:21:31 +0200 Subject: [PATCH] Various small bugfixes and enhancements (#1279) * fix typo * add fallback when binding of webserver fails * cleanup requirements * add permissions check * fix search * check for url on announcements --- music_assistant/server/controllers/music.py | 14 ++++---- music_assistant/server/controllers/players.py | 3 ++ music_assistant/server/helpers/process.py | 32 +++++++++++------ music_assistant/server/helpers/webserver.py | 14 ++++++-- .../server/models/music_provider.py | 4 +-- .../server/providers/deezer/__init__.py | 11 +----- .../server/providers/jellyfin/__init__.py | 5 +-- .../providers/opensubsonic/sonic_provider.py | 17 ++++----- .../server/providers/plex/__init__.py | 12 ++----- .../server/providers/qobuz/__init__.py | 17 ++++++--- .../server/providers/radiobrowser/__init__.py | 14 ++------ .../server/providers/soundcloud/__init__.py | 24 +++++++------ .../server/providers/spotify/__init__.py | 36 ++++++++++--------- .../server/providers/tidal/__init__.py | 15 ++++++-- .../server/providers/tidal/helpers.py | 12 +++---- .../server/providers/ytmusic/__init__.py | 18 ++++++---- pyproject.toml | 5 ++- requirements_all.txt | 2 +- 18 files changed, 136 insertions(+), 119 deletions(-) diff --git a/music_assistant/server/controllers/music.py b/music_assistant/server/controllers/music.py index 4eac6828..07d3d81b 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/server/controllers/music.py @@ -178,6 +178,8 @@ class MusicController(CoreController): :param media_types: A list of media_types to include. :param limit: number of items to return in the search (per type). """ + if not media_types: + media_types = MediaType.ALL # Check if the search query is a streaming provider public shareable URL try: media_type, provider_instance_id_or_domain, item_id = await parse_uri( @@ -231,13 +233,13 @@ class MusicController(CoreController): for sublist in zip_longest(*[x.artists for x in results_per_provider]) for item in sublist if item is not None - ], + ][:limit], albums=[ item for sublist in zip_longest(*[x.albums for x in results_per_provider]) for item in sublist if item is not None - ], + ][:limit], tracks=[ item for sublist in zip_longest(*[x.tracks for x in results_per_provider]) @@ -249,20 +251,20 @@ class MusicController(CoreController): for sublist in zip_longest(*[x.playlists for x in results_per_provider]) for item in sublist if item is not None - ], + ][:limit], radio=[ item for sublist in zip_longest(*[x.radio for x in results_per_provider]) for item in sublist if item is not None - ], + ][:limit], ) async def search_provider( self, search_query: str, provider_instance_id_or_domain: str, - media_types: list[MediaType] = MediaType.ALL, + media_types: list[MediaType], limit: int = 10, ) -> SearchResults: """Perform search on given provider. @@ -271,7 +273,7 @@ class MusicController(CoreController): :param provider_instance_id_or_domain: instance_id or domain of the provider to perform the search on. :param provider_instance: instance id of the provider to perform the search on. - :param media_types: A list of media_types to include. All types if None. + :param media_types: A list of media_types to include. :param limit: number of items to return in the search (per type). """ prov = self.mass.get_provider(provider_instance_id_or_domain) diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index 18b9b1e8..55bd130c 100644 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -28,6 +28,7 @@ from music_assistant.common.models.enums import ( ) from music_assistant.common.models.errors import ( AlreadyRegisteredError, + PlayerCommandFailed, PlayerUnavailableError, ProviderUnavailableError, UnsupportedFeaturedException, @@ -441,6 +442,8 @@ class PlayerController(CoreController): player = self.get(player_id, True) if player.announcement_in_progress: return + if not url.startswith("http"): + raise PlayerCommandFailed("Only URLs are supported for announcements") try: # mark announcement_in_progress on player player.announcement_in_progress = True diff --git a/music_assistant/server/helpers/process.py b/music_assistant/server/helpers/process.py index 466fe4b6..b82e0cd8 100644 --- a/music_assistant/server/helpers/process.py +++ b/music_assistant/server/helpers/process.py @@ -22,7 +22,7 @@ from typing import Self from music_assistant.constants import MASS_LOGGER_NAME LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.helpers.process") - +PRIVILEGED = True DEFAULT_CHUNKSIZE = 64000 @@ -93,16 +93,26 @@ class AsyncProcess: async def start(self) -> None: """Perform Async init of process.""" - self.proc = await asyncio.create_subprocess_exec( - *self._args, - stdin=asyncio.subprocess.PIPE if self._stdin is True else self._stdin, - stdout=asyncio.subprocess.PIPE if self._stdout is True else self._stdout, - stderr=asyncio.subprocess.PIPE if self._stderr is True else self._stderr, - # because we're exchanging big amounts of (audio) data with pipes - # it makes sense to extend the pipe size and (buffer) limits a bit - limit=1000000, - pipesize=1000000, - ) + for attempt in range(2): + try: + self.proc = await asyncio.create_subprocess_exec( + *self._args, + stdin=asyncio.subprocess.PIPE if self._stdin is True else self._stdin, + stdout=asyncio.subprocess.PIPE if self._stdout is True else self._stdout, + stderr=asyncio.subprocess.PIPE if self._stderr is True else self._stderr, + # because we're exchanging big amounts of (audio) data with pipes + # it makes sense to extend the pipe size and (buffer) limits a bit + limit=1000000 if attempt == 0 else 65536, + pipesize=1000000 if attempt == 0 else -1, + ) + except PermissionError: + if attempt > 0: + raise + LOGGER.error( + "Detected that you are running the (docker) container without " + "permissive access rights. This will impact performance !" + ) + self.logger.debug("Process %s started with PID %s", self.name, self.proc.pid) async def iter_chunked(self, n: int = DEFAULT_CHUNKSIZE) -> AsyncGenerator[bytes, None]: diff --git a/music_assistant/server/helpers/webserver.py b/music_assistant/server/helpers/webserver.py index b5dbe467..96d193d1 100644 --- a/music_assistant/server/helpers/webserver.py +++ b/music_assistant/server/helpers/webserver.py @@ -68,8 +68,18 @@ class Webserver: await self._apprunner.setup() # set host to None to bind to all addresses on both IPv4 and IPv6 host = None if bind_ip == "0.0.0.0" else bind_ip - self._tcp_site = web.TCPSite(self._apprunner, host=host, port=bind_port) - await self._tcp_site.start() + try: + self._tcp_site = web.TCPSite(self._apprunner, host=host, port=bind_port) + await self._tcp_site.start() + except OSError: + if host is None: + raise + # the configured interface is not available, retry on all interfaces + self.logger.error( + "Could not bind to %s, will start on all interfaces as fallback!", host + ) + self._tcp_site = web.TCPSite(self._apprunner, host=None, port=bind_port) + await self._tcp_site.start() async def close(self) -> None: """Cleanup on exit.""" diff --git a/music_assistant/server/models/music_provider.py b/music_assistant/server/models/music_provider.py index 52251952..e99ec7b0 100644 --- a/music_assistant/server/models/music_provider.py +++ b/music_assistant/server/models/music_provider.py @@ -59,13 +59,13 @@ class MusicProvider(Provider): async def search( self, search_query: str, - media_types: list[MediaType] | None = None, + media_types: list[MediaType], limit: int = 5, ) -> SearchResults: """Perform search on musicprovider. :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. + :param media_types: A list of media_types to include. :param limit: Number of items to return in the search (per type). """ if ProviderFeature.SEARCH in self.supported_features: diff --git a/music_assistant/server/providers/deezer/__init__.py b/music_assistant/server/providers/deezer/__init__.py index 2630a98f..635eea1f 100644 --- a/music_assistant/server/providers/deezer/__init__.py +++ b/music_assistant/server/providers/deezer/__init__.py @@ -205,22 +205,13 @@ class DeezerProvider(MusicProvider): # pylint: disable=W0223 return SUPPORTED_FEATURES async def search( - self, search_query: str, media_types=list[MediaType] | None, limit: int = 5 + self, search_query: str, media_types=list[MediaType], limit: int = 5 ) -> SearchResults: """Perform search on music provider. :param search_query: Search query. :param media_types: A list of media_types to include. All types if None. """ - # If no media_types are provided, search for all types - if not media_types: - media_types = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST, - ] - # Create a task for each media_type tasks = {} diff --git a/music_assistant/server/providers/jellyfin/__init__.py b/music_assistant/server/providers/jellyfin/__init__.py index 0470ba3d..cf447e44 100644 --- a/music_assistant/server/providers/jellyfin/__init__.py +++ b/music_assistant/server/providers/jellyfin/__init__.py @@ -522,7 +522,7 @@ class JellyfinProvider(MusicProvider): async def search( self, search_query: str, - media_types: list[MediaType] | None = None, + media_types: list[MediaType], limit: int = 20, ) -> SearchResults: """Perform search on the plex library. @@ -531,9 +531,6 @@ class JellyfinProvider(MusicProvider): :param media_types: A list of media_types to include. All types if None. :param limit: Number of items to return in the search (per type). """ - if not media_types: - media_types = [MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST] - tasks = {} async with TaskGroup() as tg: diff --git a/music_assistant/server/providers/opensubsonic/sonic_provider.py b/music_assistant/server/providers/opensubsonic/sonic_provider.py index 5e9a28d5..eff3b731 100644 --- a/music_assistant/server/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/server/providers/opensubsonic/sonic_provider.py @@ -438,19 +438,14 @@ class OpenSonicProvider(MusicProvider): return await asyncio.to_thread(_get_cover_art) async def search( - self, search_query: str, media_types: list[MediaType] | None = None, limit: int = 20 + self, search_query: str, media_types: list[MediaType], limit: int = 20 ) -> SearchResults: """Search the sonic library.""" - artists = limit - albums = limit - songs = limit - if media_types: - if MediaType.ARTIST not in media_types: - artists = 0 - if MediaType.ALBUM not in media_types: - albums = 0 - if MediaType.TRACK not in media_types: - songs = 0 + artists = limit if MediaType.ARTIST in media_types else 0 + albums = limit if MediaType.ALBUM in media_types else 0 + songs = limit if MediaType.TRACK in media_types else 0 + if not (artists or albums or songs): + return SearchResults() answer = await self._run_async( self._conn.search3, query=search_query, diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/server/providers/plex/__init__.py index a0b6d0b2..0938cf2e 100644 --- a/music_assistant/server/providers/plex/__init__.py +++ b/music_assistant/server/providers/plex/__init__.py @@ -582,23 +582,15 @@ class PlexProvider(MusicProvider): async def search( self, search_query: str, - media_types: list[MediaType] | None = None, + media_types: list[MediaType], limit: int = 20, ) -> SearchResults: """Perform search on the plex library. :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. + :param media_types: A list of media_types to include. :param limit: Number of items to return in the search (per type). """ - if not media_types: - media_types = [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST, - ] - tasks = {} async with TaskGroup() as tg: diff --git a/music_assistant/server/providers/qobuz/__init__.py b/music_assistant/server/providers/qobuz/__init__.py index 8623b128..c2a23d53 100644 --- a/music_assistant/server/providers/qobuz/__init__.py +++ b/music_assistant/server/providers/qobuz/__init__.py @@ -143,7 +143,7 @@ class QobuzProvider(MusicProvider): return SUPPORTED_FEATURES async def search( - self, search_query: str, media_types=list[MediaType] | None, limit: int = 5 + self, search_query: str, media_types=list[MediaType], limit: int = 5 ) -> SearchResults: """Perform search on musicprovider. @@ -152,6 +152,13 @@ class QobuzProvider(MusicProvider): :param limit: Number of items to return in the search (per type). """ result = SearchResults() + media_types = [ + x + for x in media_types + if x in (MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST) + ] + if not media_types: + return result params = {"query": search_query, "limit": limit} if len(media_types) == 1: # qobuz does not support multiple searchtypes, falls back to all if no type given @@ -164,25 +171,25 @@ class QobuzProvider(MusicProvider): if media_types[0] == MediaType.PLAYLIST: params["type"] = "playlists" if searchresult := await self._get_data("catalog/search", **params): - if "artists" in searchresult: + if "artists" in searchresult and MediaType.ARTIST in media_types: result.artists += [ self._parse_artist(item) for item in searchresult["artists"]["items"] if (item and item["id"]) ] - if "albums" in searchresult: + if "albums" in searchresult and MediaType.ALBUM in media_types: result.albums += [ await self._parse_album(item) for item in searchresult["albums"]["items"] if (item and item["id"]) ] - if "tracks" in searchresult: + if "tracks" in searchresult and MediaType.TRACK in media_types: result.tracks += [ await self._parse_track(item) for item in searchresult["tracks"]["items"] if (item and item["id"]) ] - if "playlists" in searchresult: + if "playlists" in searchresult and MediaType.PLAYLIST in media_types: result.playlists += [ self._parse_playlist(item) for item in searchresult["playlists"]["items"] diff --git a/music_assistant/server/providers/radiobrowser/__init__.py b/music_assistant/server/providers/radiobrowser/__init__.py index c7659461..711b3313 100644 --- a/music_assistant/server/providers/radiobrowser/__init__.py +++ b/music_assistant/server/providers/radiobrowser/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from time import time from typing import TYPE_CHECKING from radios import FilterBy, Order, RadioBrowser, RadioBrowserError @@ -84,7 +83,7 @@ class RadioBrowserProvider(MusicProvider): self.logger.exception("%s", err) async def search( - self, search_query: str, media_types=list[MediaType] | None, limit: int = 10 + self, search_query: str, media_types=list[MediaType], limit: int = 10 ) -> SearchResults: """Perform search on musicprovider. @@ -93,18 +92,11 @@ class RadioBrowserProvider(MusicProvider): :param limit: Number of items to return in the search (per type). """ result = SearchResults() - searchtypes = [] - if MediaType.RADIO in media_types: - searchtypes.append("radio") - - time_start = time() + if MediaType.RADIO not in media_types: + return result searchresult = await self.radios.search(name=search_query, limit=limit) - self.logger.debug( - "Processing RadioBrowser search took %s seconds", - round(time() - time_start, 2), - ) for item in searchresult: result.radio.append(await self._parse_radio(item)) diff --git a/music_assistant/server/providers/soundcloud/__init__.py b/music_assistant/server/providers/soundcloud/__init__.py index 5a5482fa..cb94eca3 100644 --- a/music_assistant/server/providers/soundcloud/__init__.py +++ b/music_assistant/server/providers/soundcloud/__init__.py @@ -123,12 +123,12 @@ class SoundcloudMusicProvider(MusicProvider): return await asyncio.to_thread(call, *args, **kwargs) async def search( - self, search_query: str, media_types=list[MediaType] | None, limit: int = 10 + self, search_query: str, media_types=list[MediaType], limit: int = 10 ) -> SearchResults: """Perform search on musicprovider. :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. + :param media_types: A list of media_types to include. :param limit: Number of items to return in the search (per type). """ result = SearchResults() @@ -140,23 +140,25 @@ class SoundcloudMusicProvider(MusicProvider): if MediaType.PLAYLIST in media_types: searchtypes.append("playlist") - time_start = time.time() + media_types = [ + x for x in media_types if x in (MediaType.ARTIST, MediaType.TRACK, MediaType.PLAYLIST) + ] + if not media_types: + return result searchresult = await self._soundcloud.search(search_query, limit) - self.logger.debug( - "Processing Soundcloud search took %s seconds", - round(time.time() - time_start, 2), - ) - for item in searchresult["collection"]: media_type = item["kind"] if media_type == "user": - result.artists.append(await self._parse_artist(item)) + if MediaType.ARTIST in media_types: + result.artists.append(await self._parse_artist(item)) elif media_type == "track": - result.tracks.append(await self._parse_track(item)) + if MediaType.TRACK in media_types: + result.tracks.append(await self._parse_track(item)) elif media_type == "playlist": - result.playlists.append(await self._parse_playlist(item)) + if MediaType.PLAYLIST in media_types: + result.playlists.append(await self._parse_playlist(item)) return result diff --git a/music_assistant/server/providers/spotify/__init__.py b/music_assistant/server/providers/spotify/__init__.py index 55ffbaea..8e1ea762 100644 --- a/music_assistant/server/providers/spotify/__init__.py +++ b/music_assistant/server/providers/spotify/__init__.py @@ -161,15 +161,15 @@ class SpotifyProvider(MusicProvider): ) async def search( - self, search_query: str, media_types=list[MediaType] | None, limit: int = 5 + self, search_query: str, media_types=list[MediaType], limit: int = 5 ) -> SearchResults: """Perform search on musicprovider. :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. + :param media_types: A list of media_types to include. :param limit: Number of items to return in the search (per type). """ - result = SearchResults() + searchresult = SearchResults() searchtypes = [] if MediaType.ARTIST in media_types: searchtypes.append("artist") @@ -179,34 +179,36 @@ class SpotifyProvider(MusicProvider): searchtypes.append("track") if MediaType.PLAYLIST in media_types: searchtypes.append("playlist") + if not searchtypes: + return searchresult searchtype = ",".join(searchtypes) search_query = search_query.replace("'", "") - searchresult = await self._get_data("search", q=search_query, type=searchtype, limit=limit) - if "artists" in searchresult: - result.artists += [ + api_result = await self._get_data("search", q=search_query, type=searchtype, limit=limit) + if "artists" in api_result: + searchresult.artists += [ self._parse_artist(item) - for item in searchresult["artists"]["items"] + for item in api_result["artists"]["items"] if (item and item["id"] and item["name"]) ] - if "albums" in searchresult: - result.albums += [ + if "albums" in api_result: + searchresult.albums += [ self._parse_album(item) - for item in searchresult["albums"]["items"] + for item in api_result["albums"]["items"] if (item and item["id"]) ] - if "tracks" in searchresult: - result.tracks += [ + if "tracks" in api_result: + searchresult.tracks += [ self._parse_track(item) - for item in searchresult["tracks"]["items"] + for item in api_result["tracks"]["items"] if (item and item["id"]) ] - if "playlists" in searchresult: - result.playlists += [ + if "playlists" in api_result: + searchresult.playlists += [ self._parse_playlist(item) - for item in searchresult["playlists"]["items"] + for item in api_result["playlists"]["items"] if (item and item["id"]) ] - return result + return searchresult async def get_library_artists(self) -> AsyncGenerator[Artist, None]: """Retrieve library artists from spotify.""" diff --git a/music_assistant/server/providers/tidal/__init__.py b/music_assistant/server/providers/tidal/__init__.py index ef510dc7..82f80d64 100644 --- a/music_assistant/server/providers/tidal/__init__.py +++ b/music_assistant/server/providers/tidal/__init__.py @@ -255,19 +255,28 @@ class TidalProvider(MusicProvider): async def search( self, search_query: str, - media_types: list[MediaType] | None = None, + media_types: list[MediaType], limit: int = 5, ) -> SearchResults: """Perform search on musicprovider. :param search_query: Search query. - :param media_types: A list of media_types to include. All types if None. + :param media_types: A list of media_types to include. :param limit: Number of items to return in the search (per type). """ + parsed_results = SearchResults() + media_types = [ + x + for x in media_types + if x in (MediaType.ARTIST, MediaType.ALBUM, MediaType.TRACK, MediaType.PLAYLIST) + ] + if not media_types: + return parsed_results + tidal_session = await self._get_tidal_session() search_query = search_query.replace("'", "") results = await search(tidal_session, search_query, media_types, limit) - parsed_results = SearchResults() + if results["artists"]: for artist in results["artists"]: parsed_results.artists.append(self._parse_artist(artist)) diff --git a/music_assistant/server/providers/tidal/helpers.py b/music_assistant/server/providers/tidal/helpers.py index 140a6cd3..2286a900 100644 --- a/music_assistant/server/providers/tidal/helpers.py +++ b/music_assistant/server/providers/tidal/helpers.py @@ -337,7 +337,7 @@ async def get_similar_tracks( async def search( session: TidalSession, query: str, - media_types: list[MediaType] | None = None, + media_types: list[MediaType], limit: int = 50, offset: int = 0, ) -> dict[str, str]: @@ -345,16 +345,16 @@ async def search( def inner() -> dict[str, str]: search_types = [] - if media_types and MediaType.ARTIST in media_types: + if MediaType.ARTIST in media_types: search_types.append(TidalArtist) - if media_types and MediaType.ALBUM in media_types: + if MediaType.ALBUM in media_types: search_types.append(TidalAlbum) - if media_types and MediaType.TRACK in media_types: + if MediaType.TRACK in media_types: search_types.append(TidalTrack) - if media_types and MediaType.PLAYLIST in media_types: + if MediaType.PLAYLIST in media_types: search_types.append(TidalPlaylist) - models = search_types if search_types else None + models = search_types results: dict[str, str] = session.search(query, models, limit, offset) return results diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/server/providers/ytmusic/__init__.py index 5e2d50bb..8ee08ddd 100644 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ b/music_assistant/server/providers/ytmusic/__init__.py @@ -212,7 +212,7 @@ class YoutubeMusicProvider(MusicProvider): return SUPPORTED_FEATURES async def search( - self, search_query: str, media_types=list[MediaType] | None, limit: int = 5 + self, search_query: str, media_types=list[MediaType], limit: int = 5 ) -> SearchResults: """Perform search on musicprovider. @@ -220,6 +220,7 @@ class YoutubeMusicProvider(MusicProvider): :param media_types: A list of media_types to include. All types if None. :param limit: Number of items to return in the search (per type). """ + parsed_results = SearchResults() ytm_filter = None if len(media_types) == 1: # YTM does not support multiple searchtypes, falls back to all if no type given @@ -231,20 +232,25 @@ class YoutubeMusicProvider(MusicProvider): ytm_filter = "songs" if media_types[0] == MediaType.PLAYLIST: ytm_filter = "playlists" + if media_types[0] == MediaType.RADIO: + # bit of an edge case but still good to handle + return parsed_results results = await search( query=search_query, ytm_filter=ytm_filter, limit=limit, language=self.language ) parsed_results = SearchResults() for result in results: try: - if result["resultType"] == "artist": + if result["resultType"] == "artist" and MediaType.ARTIST in media_types: parsed_results.artists.append(await self._parse_artist(result)) - elif result["resultType"] == "album": + elif result["resultType"] == "album" and MediaType.ALBUM in media_types: parsed_results.albums.append(await self._parse_album(result)) - elif result["resultType"] == "playlist": + elif result["resultType"] == "playlist" and MediaType.PLAYLIST in media_types: parsed_results.playlists.append(await self._parse_playlist(result)) - elif result["resultType"] in ("song", "video") and ( - track := await self._parse_track(result) + elif ( + result["resultType"] in ("song", "video") + and MediaType.TRACK in media_types + and (track := await self._parse_track(result)) ): parsed_results.tracks.append(track) except InvalidDataError: diff --git a/pyproject.toml b/pyproject.toml index 45f5c829..953d1adc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ server = [ "asyncio-throttle==1.0.2", "aiofiles==23.2.1", "aiorun==2023.7.2", + "certifi==2024.2.2", "colorlog==6.8.2", "aiosqlite==0.20.0", "python-slugify==8.0.4", @@ -32,8 +33,6 @@ server = [ "memory-tempfile==2.2.3", "music-assistant-frontend==2.5.2", "pillow==10.3.0", - "pyatv==0.14.5", - "soundcloudpy==0.1.0", "unidecode==1.3.8", "xmltodict==0.13.0", "orjson==3.10.3", @@ -43,7 +42,6 @@ server = [ "ifaddr==0.2.0", ] test = [ - "black==24.4.2", "codespell==2.2.6", "isort==5.13.2", "mypy==1.10.0", @@ -53,6 +51,7 @@ test = [ "pytest==8.2.0", "pytest-aiohttp==1.0.5", "pytest-cov==5.0.0", + "tomli==2.0.1", "ruff==0.4.3", ] diff --git a/requirements_all.txt b/requirements_all.txt index 4dd53cd8..b3de2a1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,6 +9,7 @@ aioslimproto==3.0.1 aiosqlite==0.20.0 async-upnp-client==0.38.3 asyncio-throttle==1.0.2 +certifi==2024.2.2 colorlog==6.8.2 cryptography==42.0.7 deezer-python-async==0.3.0 @@ -25,7 +26,6 @@ orjson==3.10.3 pillow==10.3.0 plexapi==4.15.12 py-opensonic==5.0.5 -pyatv==0.14.5 PyChromecast==14.0.1 pycryptodome==3.20.0 python-fullykiosk==0.0.12 -- 2.34.1