From 36a706fcdf7a5a16e04fef5fe0d599b1ef1d37d3 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 2 Nov 2024 17:20:16 +0100 Subject: [PATCH] Feat: Reorganize repository to contain only the server code --- .release-please-config-dev.json | 11 - .release-please-config-stable.json | 8 - .release-please-manifest-dev.json | 3 - .release-please-manifest-stable.json | 3 - music_assistant/__init__.py | 2 + music_assistant/__main__.py | 6 +- music_assistant/client/__init__.py | 3 - music_assistant/client/client.py | 396 ---------- music_assistant/client/config.py | 234 ------ music_assistant/client/connection.py | 130 ---- music_assistant/client/exceptions.py | 55 -- music_assistant/client/music.py | 570 --------------- music_assistant/client/player_queues.py | 251 ------- music_assistant/client/players.py | 213 ------ music_assistant/common/__init__.py | 1 - music_assistant/common/helpers/__init__.py | 1 - music_assistant/common/models/__init__.py | 1 - music_assistant/common/models/api.py | 78 -- .../common/models/config_entries.py | 677 ------------------ music_assistant/common/models/enums.py | 460 ------------ music_assistant/common/models/errors.py | 135 ---- music_assistant/common/models/event.py | 21 - music_assistant/common/models/media_items.py | 585 --------------- music_assistant/common/models/player.py | 171 ----- music_assistant/common/models/player_queue.py | 87 --- music_assistant/common/models/provider.py | 90 --- music_assistant/common/models/queue_item.py | 108 --- .../common/models/streamdetails.py | 73 -- music_assistant/constants.py | 365 +++++++++- .../{server => }/controllers/__init__.py | 0 .../{server => }/controllers/cache.py | 13 +- .../{server => }/controllers/config.py | 22 +- .../controllers/media/__init__.py | 0 .../{server => }/controllers/media/albums.py | 15 +- .../{server => }/controllers/media/artists.py | 15 +- .../{server => }/controllers/media/base.py | 13 +- .../controllers/media/playlists.py | 18 +- .../{server => }/controllers/media/radio.py | 9 +- .../{server => }/controllers/media/tracks.py | 13 +- .../{server => }/controllers/metadata.py | 33 +- .../{server => }/controllers/music.py | 30 +- .../{server => }/controllers/player_queues.py | 40 +- .../{server => }/controllers/players.py | 41 +- .../{server => }/controllers/streams.py | 37 +- .../{server => }/controllers/webserver.py | 25 +- .../{server => }/helpers/__init__.py | 0 music_assistant/{server => }/helpers/api.py | 0 .../{server => }/helpers/app_vars.py | 2 +- music_assistant/{server => }/helpers/audio.py | 23 +- music_assistant/{server => }/helpers/auth.py | 7 +- .../{server => }/helpers/compare.py | 5 +- .../{server => }/helpers/database.py | 0 .../{common => }/helpers/datetime.py | 0 .../{server => }/helpers/didl_lite.py | 5 +- .../{server => }/helpers/ffmpeg.py | 7 +- .../{common => }/helpers/global_cache.py | 0 .../{server => }/helpers/images.py | 11 +- music_assistant/{common => }/helpers/json.py | 0 .../{server => }/helpers/logging.py | 0 .../{server => }/helpers/playlists.py | 6 +- .../{server => }/helpers/process.py | 0 .../helpers/resources/announce.mp3 | Bin .../helpers/resources/fallback_fanart.jpeg | Bin .../{server => }/helpers/resources/logo.png | Bin .../helpers/resources/silence.mp3 | Bin music_assistant/{server => }/helpers/tags.py | 10 +- .../{server => }/helpers/throttle_retry.py | 5 +- music_assistant/{common => }/helpers/uri.py | 9 +- music_assistant/{common => }/helpers/util.py | 364 ++++++++-- .../{server => }/helpers/webserver.py | 0 music_assistant/{server/server.py => mass.py} | 44 +- .../{server => }/models/__init__.py | 11 +- .../{server => }/models/core_controller.py | 14 +- .../{server => }/models/metadata_provider.py | 4 +- .../{server => }/models/music_provider.py | 9 +- .../{server => }/models/player_provider.py | 14 +- music_assistant/{server => }/models/plugin.py | 0 .../{server => }/models/provider.py | 8 +- .../{server => }/providers/__init__.py | 0 .../_template_music_provider/__init__.py | 19 +- .../_template_music_provider/icon.svg | 0 .../_template_music_provider/manifest.json | 0 .../_template_player_provider/__init__.py | 22 +- .../_template_player_provider/icon.svg | 0 .../_template_player_provider/manifest.json | 0 .../providers/airplay/__init__.py | 22 +- .../airplay/bin/cliraop-linux-aarch64 | Bin .../airplay/bin/cliraop-linux-x86_64 | Bin .../providers/airplay/bin/cliraop-macos-arm64 | Bin .../{server => }/providers/airplay/const.py | 4 +- .../{server => }/providers/airplay/helpers.py | 0 .../providers/airplay/manifest.json | 0 .../{server => }/providers/airplay/player.py | 2 +- .../providers/airplay/provider.py | 35 +- .../{server => }/providers/airplay/raop.py | 15 +- .../providers/apple_music/__init__.py | 40 +- .../providers/apple_music/bin/README.md | 0 .../providers/apple_music/icon.svg | 0 .../providers/apple_music/manifest.json | 0 .../providers/bluesound/__init__.py | 33 +- .../{server => }/providers/bluesound/icon.svg | 0 .../providers/bluesound/manifest.json | 0 .../providers/builtin/__init__.py | 39 +- .../{server => }/providers/builtin/icon.svg | 0 .../providers/builtin/manifest.json | 0 .../providers/chromecast/__init__.py | 31 +- .../providers/chromecast/helpers.py | 0 .../providers/chromecast/manifest.json | 0 .../{server => }/providers/deezer/__init__.py | 30 +- .../providers/deezer/gw_client.py | 4 +- .../{server => }/providers/deezer/icon.svg | 0 .../providers/deezer/manifest.json | 0 .../{server => }/providers/dlna/__init__.py | 32 +- .../{server => }/providers/dlna/helpers.py | 2 +- .../{server => }/providers/dlna/icon.svg | 0 .../{server => }/providers/dlna/manifest.json | 0 .../providers/fanarttv/__init__.py | 25 +- .../providers/fanarttv/manifest.json | 0 .../providers/filesystem_local/__init__.py | 45 +- .../providers/filesystem_local/helpers.py | 2 +- .../providers/filesystem_local/manifest.json | 0 .../providers/filesystem_smb/__init__.py | 22 +- .../providers/filesystem_smb/manifest.json | 0 .../providers/fully_kiosk/__init__.py | 26 +- .../providers/fully_kiosk/manifest.json | 0 .../{server => }/providers/hass/__init__.py | 19 +- .../{server => }/providers/hass/icon.svg | 0 .../{server => }/providers/hass/manifest.json | 0 .../providers/hass_players/__init__.py | 31 +- .../providers/hass_players/icon.svg | 0 .../providers/hass_players/manifest.json | 0 .../providers/jellyfin/__init__.py | 29 +- .../{server => }/providers/jellyfin/const.py | 4 +- .../{server => }/providers/jellyfin/icon.svg | 0 .../providers/jellyfin/manifest.json | 0 .../providers/jellyfin/parsers.py | 7 +- .../providers/musicbrainz/__init__.py | 31 +- .../providers/musicbrainz/icon.svg | 0 .../providers/musicbrainz/icon_dark.svg | 0 .../providers/musicbrainz/manifest.json | 0 .../providers/opensubsonic/__init__.py | 16 +- .../providers/opensubsonic/icon.svg | 0 .../providers/opensubsonic/manifest.json | 0 .../providers/opensubsonic/sonic_provider.py | 16 +- .../providers/player_group/__init__.py | 31 +- .../providers/player_group/manifest.json | 0 .../providers/player_group/ugp_stream.py | 9 +- .../{server => }/providers/plex/__init__.py | 42 +- .../{server => }/providers/plex/helpers.py | 2 +- .../{server => }/providers/plex/icon.svg | 0 .../{server => }/providers/plex/manifest.json | 0 .../{server => }/providers/qobuz/__init__.py | 37 +- .../{server => }/providers/qobuz/icon.svg | 0 .../providers/qobuz/icon_dark.svg | 0 .../providers/qobuz/manifest.json | 0 .../providers/radiobrowser/__init__.py | 32 +- .../providers/radiobrowser/manifest.json | 0 .../providers/siriusxm/__init__.py | 30 +- .../{server => }/providers/siriusxm/icon.svg | 0 .../providers/siriusxm/icon_dark.svg | 0 .../providers/siriusxm/manifest.json | 0 .../providers/slimproto/__init__.py | 28 +- .../{server => }/providers/slimproto/icon.svg | 0 .../providers/slimproto/manifest.json | 0 .../slimproto/multi_client_stream.py | 7 +- .../providers/snapcast/__init__.py | 38 +- .../{server => }/providers/snapcast/icon.svg | 0 .../providers/snapcast/manifest.json | 0 .../snapweb/10-seconds-of-silence.mp3 | Bin .../snapcast/snapweb/3rd-party/libflac.js | 0 .../providers/snapcast/snapweb/config.js | 0 .../providers/snapcast/snapweb/favicon.ico | Bin .../providers/snapcast/snapweb/index.html | 0 .../snapcast/snapweb/launcher-icon.png | Bin .../providers/snapcast/snapweb/manifest.json | 0 .../providers/snapcast/snapweb/mute_icon.png | Bin .../providers/snapcast/snapweb/play.png | Bin .../snapcast/snapweb/snapcast-512.png | Bin .../providers/snapcast/snapweb/snapcontrol.js | 0 .../providers/snapcast/snapweb/snapstream.js | 0 .../snapcast/snapweb/speaker_icon.png | Bin .../providers/snapcast/snapweb/stop.png | Bin .../providers/snapcast/snapweb/styles.css | 0 .../{server => }/providers/sonos/__init__.py | 10 +- .../{server => }/providers/sonos/const.py | 3 +- .../{server => }/providers/sonos/helpers.py | 0 .../{server => }/providers/sonos/icon.svg | 0 .../providers/sonos/manifest.json | 0 .../{server => }/providers/sonos/player.py | 8 +- .../{server => }/providers/sonos/provider.py | 18 +- .../providers/sonos_s1/__init__.py | 30 +- .../providers/sonos_s1/helpers.py | 3 +- .../{server => }/providers/sonos_s1/icon.png | Bin .../{server => }/providers/sonos_s1/icon.svg | 0 .../providers/sonos_s1/manifest.json | 0 .../{server => }/providers/sonos_s1/player.py | 11 +- .../providers/soundcloud/__init__.py | 27 +- .../providers/soundcloud/icon.svg | 0 .../providers/soundcloud/manifest.json | 0 .../providers/spotify/__init__.py | 42 +- .../spotify/bin/librespot-linux-aarch64 | Bin .../spotify/bin/librespot-linux-x86_64 | Bin .../spotify/bin/librespot-macos-arm64 | Bin .../{server => }/providers/spotify/icon.svg | 0 .../providers/spotify/manifest.json | 0 .../{server => }/providers/test/__init__.py | 19 +- .../{server => }/providers/test/icon.svg | 0 .../{server => }/providers/test/manifest.json | 0 .../providers/theaudiodb/__init__.py | 31 +- .../providers/theaudiodb/manifest.json | 0 .../{server => }/providers/tidal/__init__.py | 49 +- .../{server => }/providers/tidal/helpers.py | 5 +- .../{server => }/providers/tidal/icon.svg | 0 .../providers/tidal/icon_dark.svg | 0 .../providers/tidal/manifest.json | 0 .../{server => }/providers/tunein/__init__.py | 24 +- .../{server => }/providers/tunein/icon.svg | 0 .../providers/tunein/manifest.json | 0 .../providers/ytmusic/__init__.py | 30 +- .../{server => }/providers/ytmusic/helpers.py | 2 +- .../{server => }/providers/ytmusic/icon.svg | 0 .../providers/ytmusic/manifest.json | 0 music_assistant/server/__init__.py | 3 - music_assistant/server/helpers/util.py | 297 -------- mypy.ini | 2 +- pyproject.toml | 32 +- requirements_all.txt | 5 +- scripts/gen_requirements_all.py | 8 +- scripts/setup.sh | 2 +- tests/__init__.py | 2 +- tests/common.py | 6 +- tests/conftest.py | 2 +- tests/core/__init__.py | 1 + tests/{server => core}/test_compare.py | 5 +- tests/{ => core}/test_helpers.py | 11 +- tests/{ => core}/test_radio_stream_title.py | 2 +- .../test_server_base.py} | 7 +- tests/{ => core}/test_tags.py | 2 +- .../providers/filesystem/__init__.py | 0 .../providers/filesystem/test_helpers.py | 2 +- .../providers/jellyfin/__init__.py | 0 .../jellyfin/__snapshots__/test_parsers.ambr | 0 .../jellyfin/fixtures/albums/infest.json | 0 .../fixtures/albums/this_is_christmas.json | 0 .../albums/yesterday_when_i_was_mad.json | 0 .../jellyfin/fixtures/artists/ash.json | 0 .../jellyfin/fixtures/tracks/thrown_away.json | 0 .../fixtures/tracks/where_the_bands_are.json | 0 .../fixtures/tracks/zombie_christmas.json | 0 .../providers/jellyfin/test_init.py | 8 +- .../providers/jellyfin/test_parsers.py | 2 +- tests/server/__init__.py | 1 - 252 files changed, 1542 insertions(+), 5649 deletions(-) delete mode 100644 .release-please-config-dev.json delete mode 100644 .release-please-config-stable.json delete mode 100644 .release-please-manifest-dev.json delete mode 100644 .release-please-manifest-stable.json delete mode 100644 music_assistant/client/__init__.py delete mode 100644 music_assistant/client/client.py delete mode 100644 music_assistant/client/config.py delete mode 100644 music_assistant/client/connection.py delete mode 100644 music_assistant/client/exceptions.py delete mode 100644 music_assistant/client/music.py delete mode 100644 music_assistant/client/player_queues.py delete mode 100644 music_assistant/client/players.py delete mode 100644 music_assistant/common/__init__.py delete mode 100644 music_assistant/common/helpers/__init__.py delete mode 100644 music_assistant/common/models/__init__.py delete mode 100644 music_assistant/common/models/api.py delete mode 100644 music_assistant/common/models/config_entries.py delete mode 100644 music_assistant/common/models/enums.py delete mode 100644 music_assistant/common/models/errors.py delete mode 100644 music_assistant/common/models/event.py delete mode 100644 music_assistant/common/models/media_items.py delete mode 100644 music_assistant/common/models/player.py delete mode 100644 music_assistant/common/models/player_queue.py delete mode 100644 music_assistant/common/models/provider.py delete mode 100644 music_assistant/common/models/queue_item.py delete mode 100644 music_assistant/common/models/streamdetails.py rename music_assistant/{server => }/controllers/__init__.py (100%) rename music_assistant/{server => }/controllers/cache.py (97%) rename music_assistant/{server => }/controllers/config.py (98%) rename music_assistant/{server => }/controllers/media/__init__.py (100%) rename music_assistant/{server => }/controllers/media/albums.py (97%) rename music_assistant/{server => }/controllers/media/artists.py (97%) rename music_assistant/{server => }/controllers/media/base.py (98%) rename music_assistant/{server => }/controllers/media/playlists.py (97%) rename music_assistant/{server => }/controllers/media/radio.py (94%) rename music_assistant/{server => }/controllers/media/tracks.py (98%) rename music_assistant/{server => }/controllers/metadata.py (97%) rename music_assistant/{server => }/controllers/music.py (98%) rename music_assistant/{server => }/controllers/player_queues.py (98%) rename music_assistant/{server => }/controllers/players.py (98%) rename music_assistant/{server => }/controllers/streams.py (97%) rename music_assistant/{server => }/controllers/webserver.py (95%) rename music_assistant/{server => }/helpers/__init__.py (100%) rename music_assistant/{server => }/helpers/api.py (100%) rename music_assistant/{server => }/helpers/app_vars.py (94%) rename music_assistant/{server => }/helpers/audio.py (98%) rename music_assistant/{server => }/helpers/auth.py (94%) rename music_assistant/{server => }/helpers/compare.py (99%) rename music_assistant/{server => }/helpers/database.py (100%) rename music_assistant/{common => }/helpers/datetime.py (100%) rename music_assistant/{server => }/helpers/didl_lite.py (96%) rename music_assistant/{server => }/helpers/ffmpeg.py (98%) rename music_assistant/{common => }/helpers/global_cache.py (100%) rename music_assistant/{server => }/helpers/images.py (92%) rename music_assistant/{common => }/helpers/json.py (100%) rename music_assistant/{server => }/helpers/logging.py (100%) rename music_assistant/{server => }/helpers/playlists.py (97%) rename music_assistant/{server => }/helpers/process.py (100%) rename music_assistant/{server => }/helpers/resources/announce.mp3 (100%) rename music_assistant/{server => }/helpers/resources/fallback_fanart.jpeg (100%) rename music_assistant/{server => }/helpers/resources/logo.png (100%) rename music_assistant/{server => }/helpers/resources/silence.mp3 (100%) rename music_assistant/{server => }/helpers/tags.py (98%) rename music_assistant/{server => }/helpers/throttle_retry.py (96%) rename music_assistant/{common => }/helpers/uri.py (88%) rename music_assistant/{common => }/helpers/util.py (51%) rename music_assistant/{server => }/helpers/webserver.py (100%) rename music_assistant/{server/server.py => mass.py} (95%) rename music_assistant/{server => }/models/__init__.py (82%) rename music_assistant/{server => }/models/core_controller.py (88%) rename music_assistant/{server => }/models/metadata_provider.py (91%) rename music_assistant/{server => }/models/music_provider.py (98%) rename music_assistant/{server => }/models/player_provider.py (97%) rename music_assistant/{server => }/models/plugin.py (100%) rename music_assistant/{server => }/models/provider.py (92%) rename music_assistant/{server => }/providers/__init__.py (100%) rename music_assistant/{server => }/providers/_template_music_provider/__init__.py (97%) rename music_assistant/{server => }/providers/_template_music_provider/icon.svg (100%) rename music_assistant/{server => }/providers/_template_music_provider/manifest.json (100%) rename music_assistant/{server => }/providers/_template_player_provider/__init__.py (96%) rename music_assistant/{server => }/providers/_template_player_provider/icon.svg (100%) rename music_assistant/{server => }/providers/_template_player_provider/manifest.json (100%) rename music_assistant/{server => }/providers/airplay/__init__.py (69%) rename music_assistant/{server => }/providers/airplay/bin/cliraop-linux-aarch64 (100%) rename music_assistant/{server => }/providers/airplay/bin/cliraop-linux-x86_64 (100%) rename music_assistant/{server => }/providers/airplay/bin/cliraop-macos-arm64 (100%) rename music_assistant/{server => }/providers/airplay/const.py (85%) rename music_assistant/{server => }/providers/airplay/helpers.py (100%) rename music_assistant/{server => }/providers/airplay/manifest.json (100%) rename music_assistant/{server => }/providers/airplay/player.py (96%) rename music_assistant/{server => }/providers/airplay/provider.py (96%) rename music_assistant/{server => }/providers/airplay/raop.py (97%) rename music_assistant/{server => }/providers/apple_music/__init__.py (97%) rename music_assistant/{server => }/providers/apple_music/bin/README.md (100%) rename music_assistant/{server => }/providers/apple_music/icon.svg (100%) rename music_assistant/{server => }/providers/apple_music/manifest.json (100%) rename music_assistant/{server => }/providers/bluesound/__init__.py (95%) rename music_assistant/{server => }/providers/bluesound/icon.svg (100%) rename music_assistant/{server => }/providers/bluesound/manifest.json (100%) rename music_assistant/{server => }/providers/builtin/__init__.py (96%) rename music_assistant/{server => }/providers/builtin/icon.svg (100%) rename music_assistant/{server => }/providers/builtin/manifest.json (100%) rename music_assistant/{server => }/providers/chromecast/__init__.py (97%) rename music_assistant/{server => }/providers/chromecast/helpers.py (100%) rename music_assistant/{server => }/providers/chromecast/manifest.json (100%) rename music_assistant/{server => }/providers/deezer/__init__.py (97%) rename music_assistant/{server => }/providers/deezer/gw_client.py (98%) rename music_assistant/{server => }/providers/deezer/icon.svg (100%) rename music_assistant/{server => }/providers/deezer/manifest.json (100%) rename music_assistant/{server => }/providers/dlna/__init__.py (96%) rename music_assistant/{server => }/providers/dlna/helpers.py (96%) rename music_assistant/{server => }/providers/dlna/icon.svg (100%) rename music_assistant/{server => }/providers/dlna/manifest.json (100%) rename music_assistant/{server => }/providers/fanarttv/__init__.py (88%) rename music_assistant/{server => }/providers/fanarttv/manifest.json (100%) rename music_assistant/{server => }/providers/filesystem_local/__init__.py (97%) rename music_assistant/{server => }/providers/filesystem_local/helpers.py (98%) rename music_assistant/{server => }/providers/filesystem_local/manifest.json (100%) rename music_assistant/{server => }/providers/filesystem_smb/__init__.py (92%) rename music_assistant/{server => }/providers/filesystem_smb/manifest.json (100%) rename music_assistant/{server => }/providers/fully_kiosk/__init__.py (91%) rename music_assistant/{server => }/providers/fully_kiosk/manifest.json (100%) rename music_assistant/{server => }/providers/hass/__init__.py (91%) rename music_assistant/{server => }/providers/hass/icon.svg (100%) rename music_assistant/{server => }/providers/hass/manifest.json (100%) rename music_assistant/{server => }/providers/hass_players/__init__.py (95%) rename music_assistant/{server => }/providers/hass_players/icon.svg (100%) rename music_assistant/{server => }/providers/hass_players/manifest.json (100%) rename music_assistant/{server => }/providers/jellyfin/__init__.py (96%) rename music_assistant/{server => }/providers/jellyfin/const.py (95%) rename music_assistant/{server => }/providers/jellyfin/icon.svg (100%) rename music_assistant/{server => }/providers/jellyfin/manifest.json (100%) rename music_assistant/{server => }/providers/jellyfin/parsers.py (98%) rename music_assistant/{server => }/providers/musicbrainz/__init__.py (93%) rename music_assistant/{server => }/providers/musicbrainz/icon.svg (100%) rename music_assistant/{server => }/providers/musicbrainz/icon_dark.svg (100%) rename music_assistant/{server => }/providers/musicbrainz/manifest.json (100%) rename music_assistant/{server => }/providers/opensubsonic/__init__.py (88%) rename music_assistant/{server => }/providers/opensubsonic/icon.svg (100%) rename music_assistant/{server => }/providers/opensubsonic/manifest.json (100%) rename music_assistant/{server => }/providers/opensubsonic/sonic_provider.py (98%) rename music_assistant/{server => }/providers/player_group/__init__.py (97%) rename music_assistant/{server => }/providers/player_group/manifest.json (100%) rename music_assistant/{server => }/providers/player_group/ugp_stream.py (92%) rename music_assistant/{server => }/providers/plex/__init__.py (97%) rename music_assistant/{server => }/providers/plex/helpers.py (98%) rename music_assistant/{server => }/providers/plex/icon.svg (100%) rename music_assistant/{server => }/providers/plex/manifest.json (100%) rename music_assistant/{server => }/providers/qobuz/__init__.py (96%) rename music_assistant/{server => }/providers/qobuz/icon.svg (100%) rename music_assistant/{server => }/providers/qobuz/icon_dark.svg (100%) rename music_assistant/{server => }/providers/qobuz/manifest.json (100%) rename music_assistant/{server => }/providers/radiobrowser/__init__.py (93%) rename music_assistant/{server => }/providers/radiobrowser/manifest.json (100%) rename music_assistant/{server => }/providers/siriusxm/__init__.py (92%) rename music_assistant/{server => }/providers/siriusxm/icon.svg (100%) rename music_assistant/{server => }/providers/siriusxm/icon_dark.svg (100%) rename music_assistant/{server => }/providers/siriusxm/manifest.json (100%) rename music_assistant/{server => }/providers/slimproto/__init__.py (97%) rename music_assistant/{server => }/providers/slimproto/icon.svg (100%) rename music_assistant/{server => }/providers/slimproto/manifest.json (100%) rename music_assistant/{server => }/providers/slimproto/multi_client_stream.py (94%) rename music_assistant/{server => }/providers/snapcast/__init__.py (96%) rename music_assistant/{server => }/providers/snapcast/icon.svg (100%) rename music_assistant/{server => }/providers/snapcast/manifest.json (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/10-seconds-of-silence.mp3 (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/3rd-party/libflac.js (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/config.js (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/favicon.ico (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/index.html (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/launcher-icon.png (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/manifest.json (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/mute_icon.png (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/play.png (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/snapcast-512.png (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/snapcontrol.js (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/snapstream.js (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/speaker_icon.png (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/stop.png (100%) rename music_assistant/{server => }/providers/snapcast/snapweb/styles.css (100%) rename music_assistant/{server => }/providers/sonos/__init__.py (80%) rename music_assistant/{server => }/providers/sonos/const.py (90%) rename music_assistant/{server => }/providers/sonos/helpers.py (100%) rename music_assistant/{server => }/providers/sonos/icon.svg (100%) rename music_assistant/{server => }/providers/sonos/manifest.json (100%) rename music_assistant/{server => }/providers/sonos/player.py (99%) rename music_assistant/{server => }/providers/sonos/provider.py (98%) rename music_assistant/{server => }/providers/sonos_s1/__init__.py (96%) rename music_assistant/{server => }/providers/sonos_s1/helpers.py (98%) rename music_assistant/{server => }/providers/sonos_s1/icon.png (100%) rename music_assistant/{server => }/providers/sonos_s1/icon.svg (100%) rename music_assistant/{server => }/providers/sonos_s1/manifest.json (100%) rename music_assistant/{server => }/providers/sonos_s1/player.py (98%) rename music_assistant/{server => }/providers/soundcloud/__init__.py (95%) rename music_assistant/{server => }/providers/soundcloud/icon.svg (100%) rename music_assistant/{server => }/providers/soundcloud/manifest.json (100%) rename music_assistant/{server => }/providers/spotify/__init__.py (96%) rename music_assistant/{server => }/providers/spotify/bin/librespot-linux-aarch64 (100%) rename music_assistant/{server => }/providers/spotify/bin/librespot-linux-x86_64 (100%) rename music_assistant/{server => }/providers/spotify/bin/librespot-macos-arm64 (100%) rename music_assistant/{server => }/providers/spotify/icon.svg (100%) rename music_assistant/{server => }/providers/spotify/manifest.json (100%) rename music_assistant/{server => }/providers/test/__init__.py (89%) rename music_assistant/{server => }/providers/test/icon.svg (100%) rename music_assistant/{server => }/providers/test/manifest.json (100%) rename music_assistant/{server => }/providers/theaudiodb/__init__.py (94%) rename music_assistant/{server => }/providers/theaudiodb/manifest.json (100%) rename music_assistant/{server => }/providers/tidal/__init__.py (97%) rename music_assistant/{server => }/providers/tidal/helpers.py (98%) rename music_assistant/{server => }/providers/tidal/icon.svg (100%) rename music_assistant/{server => }/providers/tidal/icon_dark.svg (100%) rename music_assistant/{server => }/providers/tidal/manifest.json (100%) rename music_assistant/{server => }/providers/tunein/__init__.py (93%) rename music_assistant/{server => }/providers/tunein/icon.svg (100%) rename music_assistant/{server => }/providers/tunein/manifest.json (100%) rename music_assistant/{server => }/providers/ytmusic/__init__.py (97%) rename music_assistant/{server => }/providers/ytmusic/helpers.py (99%) rename music_assistant/{server => }/providers/ytmusic/icon.svg (100%) rename music_assistant/{server => }/providers/ytmusic/manifest.json (100%) delete mode 100644 music_assistant/server/__init__.py delete mode 100644 music_assistant/server/helpers/util.py create mode 100644 tests/core/__init__.py rename tests/{server => core}/test_compare.py (99%) rename tests/{ => core}/test_helpers.py (91%) rename tests/{ => core}/test_radio_stream_title.py (97%) rename tests/{server/test_server.py => core/test_server_base.py} (89%) rename tests/{ => core}/test_tags.py (98%) rename tests/{server => }/providers/filesystem/__init__.py (100%) rename tests/{server => }/providers/filesystem/test_helpers.py (98%) rename tests/{server => }/providers/jellyfin/__init__.py (100%) rename tests/{server => }/providers/jellyfin/__snapshots__/test_parsers.ambr (100%) rename tests/{server => }/providers/jellyfin/fixtures/albums/infest.json (100%) rename tests/{server => }/providers/jellyfin/fixtures/albums/this_is_christmas.json (100%) rename tests/{server => }/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json (100%) rename tests/{server => }/providers/jellyfin/fixtures/artists/ash.json (100%) rename tests/{server => }/providers/jellyfin/fixtures/tracks/thrown_away.json (100%) rename tests/{server => }/providers/jellyfin/fixtures/tracks/where_the_bands_are.json (100%) rename tests/{server => }/providers/jellyfin/fixtures/tracks/zombie_christmas.json (100%) rename tests/{server => }/providers/jellyfin/test_init.py (87%) rename tests/{server => }/providers/jellyfin/test_parsers.py (96%) delete mode 100644 tests/server/__init__.py diff --git a/.release-please-config-dev.json b/.release-please-config-dev.json deleted file mode 100644 index 8d96b494d..000000000 --- a/.release-please-config-dev.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "packages": { - ".": { - "prerelease": true, - "versioning-strategy": "prerelease", - "prerelease-type": "b", - "draft": true - } - } -} diff --git a/.release-please-config-stable.json b/.release-please-config-stable.json deleted file mode 100644 index d1de93f00..000000000 --- a/.release-please-config-stable.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "packages": { - ".": { - "draft": true - } - } -} diff --git a/.release-please-manifest-dev.json b/.release-please-manifest-dev.json deleted file mode 100644 index 11094440f..000000000 --- a/.release-please-manifest-dev.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "2.4.0b2" -} diff --git a/.release-please-manifest-stable.json b/.release-please-manifest-stable.json deleted file mode 100644 index aca3a4946..000000000 --- a/.release-please-manifest-stable.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - ".": "2.3.1" -} diff --git a/music_assistant/__init__.py b/music_assistant/__init__.py index 92538754f..6632acb66 100644 --- a/music_assistant/__init__.py +++ b/music_assistant/__init__.py @@ -1 +1,3 @@ """Music Assistant: The music library manager in python.""" + +from .mass import MusicAssistant # noqa: F401 diff --git a/music_assistant/__main__.py b/music_assistant/__main__.py index 6f9ad40ec..1e989c65e 100644 --- a/music_assistant/__main__.py +++ b/music_assistant/__main__.py @@ -17,10 +17,10 @@ from aiorun import run from colorlog import ColoredFormatter -from music_assistant.common.helpers.json import json_loads +from music_assistant import MusicAssistant from music_assistant.constants import MASS_LOGGER_NAME, VERBOSE_LOG_LEVEL -from music_assistant.server import MusicAssistant -from music_assistant.server.helpers.logging import activate_log_queue_handler +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.logging import activate_log_queue_handler FORMAT_DATE: Final = "%Y-%m-%d" FORMAT_TIME: Final = "%H:%M:%S" diff --git a/music_assistant/client/__init__.py b/music_assistant/client/__init__.py deleted file mode 100644 index 731b8d528..000000000 --- a/music_assistant/client/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Music Assistant Client: Manage a Music Assistant server remotely.""" - -from .client import MusicAssistantClient # noqa: F401 diff --git a/music_assistant/client/client.py b/music_assistant/client/client.py deleted file mode 100644 index 48d6cdbaf..000000000 --- a/music_assistant/client/client.py +++ /dev/null @@ -1,396 +0,0 @@ -"""Music Assistant Client: Manage a Music Assistant server remotely.""" - -from __future__ import annotations - -import asyncio -import logging -import urllib.parse -import uuid -from collections.abc import Callable, Coroutine -from typing import TYPE_CHECKING, Any - -from music_assistant.client.exceptions import ConnectionClosed, InvalidServerVersion, InvalidState -from music_assistant.common.models.api import ( - CommandMessage, - ErrorResultMessage, - EventMessage, - ResultMessageBase, - ServerInfoMessage, - SuccessResultMessage, - parse_message, -) -from music_assistant.common.models.enums import EventType, ImageType -from music_assistant.common.models.errors import ERROR_MAP -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.media_items import ItemMapping, MediaItemType -from music_assistant.common.models.provider import ProviderInstance, ProviderManifest -from music_assistant.common.models.queue_item import QueueItem -from music_assistant.constants import API_SCHEMA_VERSION - -from .config import Config -from .connection import WebsocketsConnection -from .music import Music -from .player_queues import PlayerQueues -from .players import Players - -if TYPE_CHECKING: - from types import TracebackType - - from aiohttp import ClientSession - - from music_assistant.common.models.media_items import MediaItemImage - -EventCallBackType = Callable[[MassEvent], Coroutine[Any, Any, None] | None] -EventSubscriptionType = tuple[ - EventCallBackType, tuple[EventType, ...] | None, tuple[str, ...] | None -] - - -class MusicAssistantClient: - """Manage a Music Assistant server remotely.""" - - def __init__(self, server_url: str, aiohttp_session: ClientSession | None) -> None: - """Initialize the Music Assistant client.""" - self.server_url = server_url - self.connection = WebsocketsConnection(server_url, aiohttp_session) - self.logger = logging.getLogger(__package__) - self._result_futures: dict[str | int, asyncio.Future[Any]] = {} - self._subscribers: list[EventSubscriptionType] = [] - self._stop_called: bool = False - self._loop: asyncio.AbstractEventLoop | None = None - self._config = Config(self) - self._players = Players(self) - self._player_queues = PlayerQueues(self) - self._music = Music(self) - # below items are retrieved after connect - self._server_info: ServerInfoMessage | None = None - self._provider_manifests: dict[str, ProviderManifest] = {} - self._providers: dict[str, ProviderInstance] = {} - - @property - def server_info(self) -> ServerInfoMessage | None: - """Return info of the server we're currently connected to.""" - return self._server_info - - @property - def providers(self) -> list[ProviderInstance]: - """Return all loaded/running Providers (instances).""" - return list(self._providers.values()) - - @property - def provider_manifests(self) -> list[ProviderManifest]: - """Return all Provider manifests.""" - return list(self._provider_manifests.values()) - - @property - def config(self) -> Config: - """Return Config handler.""" - return self._config - - @property - def players(self) -> Players: - """Return Players handler.""" - return self._players - - @property - def player_queues(self) -> PlayerQueues: - """Return PlayerQueues handler.""" - return self._player_queues - - @property - def music(self) -> Music: - """Return Music handler.""" - return self._music - - def get_provider_manifest(self, domain: str) -> ProviderManifest: - """Return Provider manifests of single provider(domain).""" - return self._provider_manifests[domain] - - def get_provider( - self, provider_instance_or_domain: str, return_unavailable: bool = False - ) -> ProviderInstance | None: - """Return provider by instance id or domain.""" - # lookup by instance_id first - if prov := self._providers.get(provider_instance_or_domain): - if return_unavailable or prov.available: - return prov - if not prov.is_streaming_provider: - # no need to lookup other instances because this provider has unique data - return None - provider_instance_or_domain = prov.domain - # fallback to match on domain - # note that this can be tricky if the provider has multiple instances - # and has unique data (e.g. filesystem) - for prov in self._providers.values(): - if prov.domain != provider_instance_or_domain: - continue - if return_unavailable or prov.available: - return prov - self.logger.debug("Provider %s is not available", provider_instance_or_domain) - return None - - def get_image_url(self, image: MediaItemImage, size: int = 0) -> str: - """Get (proxied) URL for MediaItemImage.""" - assert self.server_info - if image.remotely_accessible and not size: - return image.path - if image.remotely_accessible and size: - # get url to resized image(thumb) from weserv service - return ( - f"https://images.weserv.nl/?url={urllib.parse.quote(image.path)}" - f"&w=${size}&h=${size}&fit=cover&a=attention" - ) - # return imageproxy url for images that need to be resolved - # the original path is double encoded - encoded_url = urllib.parse.quote(urllib.parse.quote(image.path)) - return ( - f"{self.server_info.base_url}/imageproxy?path={encoded_url}" - f"&provider={image.provider}&size={size}" - ) - - def get_media_item_image_url( - self, - item: MediaItemType | ItemMapping | QueueItem, - type: ImageType = ImageType.THUMB, # noqa: A002 - size: int = 0, - ) -> str | None: - """Get image URL for MediaItem, QueueItem or ItemMapping.""" - # handle queueitem with media_item attribute - if media_item := getattr(item, "media_item", None): - if img := self.music.get_media_item_image(media_item, type): - return self.get_image_url(img, size) - if img := self.music.get_media_item_image(item, type): - return self.get_image_url(img, size) - return None - - def subscribe( - self, - cb_func: EventCallBackType, - event_filter: EventType | tuple[EventType, ...] | None = None, - id_filter: str | tuple[str, ...] | None = None, - ) -> Callable[[], None]: - """Add callback to event listeners. - - Returns function to remove the listener. - :param cb_func: callback function or coroutine - :param event_filter: Optionally only listen for these events - :param id_filter: Optionally only listen for these id's (player_id, queue_id, uri) - """ - if isinstance(event_filter, EventType): - event_filter = (event_filter,) - if isinstance(id_filter, str): - id_filter = (id_filter,) - listener = (cb_func, event_filter, id_filter) - self._subscribers.append(listener) - - def remove_listener() -> None: - self._subscribers.remove(listener) - - return remove_listener - - async def connect(self) -> None: - """Connect to the remote Music Assistant Server.""" - self._loop = asyncio.get_running_loop() - if self.connection.connected: - # already connected - return - # NOTE: connect will raise when connecting failed - result = await self.connection.connect() - info = ServerInfoMessage.from_dict(result) - - # basic check for server schema version compatibility - if info.min_supported_schema_version > API_SCHEMA_VERSION: - # our schema version is too low and can't be handled by the server anymore. - await self.connection.disconnect() - msg = ( - f"Schema version is incompatible: {info.schema_version}, " - f"the server requires at least {info.min_supported_schema_version} " - " - update the Music Assistant client to a more " - "recent version or downgrade the server." - ) - raise InvalidServerVersion(msg) - - self._server_info = info - - self.logger.info( - "Connected to Music Assistant Server %s, Version %s, Schema Version %s", - info.server_id, - info.server_version, - info.schema_version, - ) - - async def send_command( - self, - command: str, - require_schema: int | None = None, - **kwargs: Any, - ) -> Any: - """Send a command and get a response.""" - if not self.connection.connected or not self._loop: - msg = "Not connected" - raise InvalidState(msg) - - if ( - require_schema is not None - and self.server_info is not None - and require_schema > self.server_info.schema_version - ): - msg = ( - "Command not available due to incompatible server version. Update the Music " - f"Assistant Server to a version that supports at least api schema {require_schema}." - ) - raise InvalidServerVersion(msg) - - command_message = CommandMessage( - message_id=uuid.uuid4().hex, - command=command, - args=kwargs, - ) - future: asyncio.Future[Any] = self._loop.create_future() - self._result_futures[command_message.message_id] = future - await self.connection.send_message(command_message.to_dict()) - try: - return await future - finally: - self._result_futures.pop(command_message.message_id) - - async def send_command_no_wait( - self, - command: str, - require_schema: int | None = None, - **kwargs: Any, - ) -> None: - """Send a command without waiting for the response.""" - if not self.server_info: - msg = "Not connected" - raise InvalidState(msg) - - if require_schema is not None and require_schema > self.server_info.schema_version: - msg = ( - "Command not available due to incompatible server version. Update the Music " - f"Assistant Server to a version that supports at least api schema {require_schema}." - ) - raise InvalidServerVersion(msg) - command_message = CommandMessage( - message_id=uuid.uuid4().hex, - command=command, - args=kwargs, - ) - await self.connection.send_message(command_message.to_dict()) - - async def start_listening(self, init_ready: asyncio.Event | None = None) -> None: - """Connect (if needed) and start listening to incoming messages from the server.""" - await self.connect() - - # fetch initial state - # we do this in a separate task to not block reading messages - async def fetch_initial_state() -> None: - self._providers = { - x["instance_id"]: ProviderInstance.from_dict(x) - for x in await self.send_command("providers") - } - self._provider_manifests = { - x["domain"]: ProviderManifest.from_dict(x) - for x in await self.send_command("providers/manifests") - } - await self._player_queues.fetch_state() - await self._players.fetch_state() - - if init_ready is not None: - init_ready.set() - - asyncio.create_task(fetch_initial_state()) - - try: - # keep reading incoming messages - while not self._stop_called: - msg = await self.connection.receive_message() - self._handle_incoming_message(msg) - except ConnectionClosed: - pass - finally: - await self.disconnect() - - async def disconnect(self) -> None: - """Disconnect the client and cleanup.""" - self._stop_called = True - # cancel all command-tasks awaiting a result - for future in self._result_futures.values(): - future.cancel() - await self.connection.disconnect() - - def _handle_incoming_message(self, raw: dict[str, Any]) -> None: - """ - Handle incoming message. - - Run all async tasks in a wrapper to log appropriately. - """ - msg = parse_message(raw) - # handle result message - if isinstance(msg, ResultMessageBase): - future = self._result_futures.get(msg.message_id) - - if future is None: - # no listener for this result - return - if isinstance(msg, SuccessResultMessage): - future.set_result(msg.result) - return - if isinstance(msg, ErrorResultMessage): - exc = ERROR_MAP[msg.error_code] - future.set_exception(exc(msg.details)) - return - - # handle EventMessage - if isinstance(msg, EventMessage): - self.logger.debug("Received event: %s", msg) - self._handle_event(msg) - return - - # Log anything we can't handle here - self.logger.debug( - "Received message with unknown type '%s': %s", - type(msg), - msg, - ) - - def _handle_event(self, event: MassEvent) -> None: - """Forward event to subscribers.""" - if self._stop_called: - return - - assert self._loop - - if event.event == EventType.PROVIDERS_UPDATED: - self._providers = {x["instance_id"]: ProviderInstance.from_dict(x) for x in event.data} - - for cb_func, event_filter, id_filter in self._subscribers: - if not (event_filter is None or event.event in event_filter): - continue - if not (id_filter is None or event.object_id in id_filter): - continue - if asyncio.iscoroutinefunction(cb_func): - asyncio.run_coroutine_threadsafe(cb_func(event), self._loop) - else: - self._loop.call_soon_threadsafe(cb_func, event) - - async def __aenter__(self) -> MusicAssistantClient: - """Initialize and connect the connection to the Music Assistant Server.""" - await self.connect() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit context manager.""" - await self.disconnect() - return None - - def __repr__(self) -> str: - """Return the representation.""" - conn_type = self.connection.__class__.__name__ - prefix = "" if self.connection.connected else "not " - return f"{type(self).__name__}(connection={conn_type}, {prefix}connected)" diff --git a/music_assistant/client/config.py b/music_assistant/client/config.py deleted file mode 100644 index 5a9019349..000000000 --- a/music_assistant/client/config.py +++ /dev/null @@ -1,234 +0,0 @@ -"""Handle Config related endpoints for Music Assistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, cast - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - CoreConfig, - PlayerConfig, - ProviderConfig, -) -from music_assistant.common.models.enums import ProviderType - -if TYPE_CHECKING: - from .client import MusicAssistantClient - - -class Config: - """Config related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - - # Provider Config related commands/functions - - async def get_provider_configs( - self, - provider_type: ProviderType | None = None, - provider_domain: str | None = None, - include_values: bool = False, - ) -> list[ProviderConfig]: - """Return all known provider configurations, optionally filtered by ProviderType.""" - return [ - ProviderConfig.from_dict(item) - for item in await self.client.send_command( - "config/providers", - provider_type=provider_type, - provider_domain=provider_domain, - include_values=include_values, - ) - ] - - async def get_provider_config(self, instance_id: str) -> ProviderConfig: - """Return (full) configuration for a single provider.""" - return ProviderConfig.from_dict( - await self.client.send_command("config/providers/get", instance_id=instance_id) - ) - - async def get_provider_config_value(self, instance_id: str, key: str) -> ConfigValueType: - """Return single configentry value for a provider.""" - return cast( - ConfigValueType, - await self.client.send_command( - "config/providers/get_value", instance_id=instance_id, key=key - ), - ) - - async def get_provider_config_entries( - self, - provider_domain: str, - instance_id: str | None = None, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to setup/configure a provider. - - provider_domain: (mandatory) domain of the provider. - instance_id: id of an existing provider instance (None for new instance setup). - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - return tuple( - ConfigEntry.from_dict(x) - for x in await self.client.send_command( - "config/providers/get_entries", - provider_domain=provider_domain, - instance_id=instance_id, - action=action, - values=values, - ) - ) - - async def save_provider_config( - self, - provider_domain: str, - values: dict[str, ConfigValueType], - instance_id: str | None = None, - ) -> ProviderConfig: - """ - Save Provider(instance) Config. - - provider_domain: (mandatory) domain of the provider. - values: the raw values for config entries that need to be stored/updated. - instance_id: id of an existing provider instance (None for new instance setup). - """ - return ProviderConfig.from_dict( - await self.client.send_command( - "config/providers/save", - provider_domain=provider_domain, - values=values, - instance_id=instance_id, - ) - ) - - async def remove_provider_config(self, instance_id: str) -> None: - """Remove ProviderConfig.""" - await self.client.send_command( - "config/providers/remove", - instance_id=instance_id, - ) - - async def reload_provider(self, instance_id: str) -> None: - """Reload provider.""" - await self.client.send_command( - "config/providers/reload", - instance_id=instance_id, - ) - - # Player Config related commands/functions - - async def get_player_configs( - self, provider: str | None = None, include_values: bool = False - ) -> list[PlayerConfig]: - """Return all known player configurations, optionally filtered by provider domain.""" - return [ - PlayerConfig.from_dict(item) - for item in await self.client.send_command( - "config/players", - provider=provider, - include_values=include_values, - ) - ] - - async def get_player_config(self, player_id: str) -> PlayerConfig: - """Return (full) configuration for a single player.""" - return PlayerConfig.from_dict( - await self.client.send_command("config/players/get", player_id=player_id) - ) - - async def get_player_config_value( - self, - player_id: str, - key: str, - ) -> ConfigValueType: - """Return single configentry value for a player.""" - return cast( - ConfigValueType, - await self.client.send_command( - "config/players/get_value", player_id=player_id, key=key - ), - ) - - async def save_player_config( - self, player_id: str, values: dict[str, ConfigValueType] - ) -> PlayerConfig: - """Save/update PlayerConfig.""" - return PlayerConfig.from_dict( - await self.client.send_command( - "config/players/save", player_id=player_id, values=values - ) - ) - - async def remove_player_config(self, player_id: str) -> None: - """Remove PlayerConfig.""" - await self.client.send_command("config/players/remove", player_id=player_id) - - # Core Controller config commands - - async def get_core_configs(self, include_values: bool = False) -> list[CoreConfig]: - """Return all core controllers config options.""" - return [ - CoreConfig.from_dict(item) - for item in await self.client.send_command( - "config/core", - include_values=include_values, - ) - ] - - async def get_core_config(self, domain: str) -> CoreConfig: - """Return configuration for a single core controller.""" - return CoreConfig.from_dict( - await self.client.send_command( - "config/core/get", - domain=domain, - ) - ) - - async def get_core_config_value(self, domain: str, key: str) -> ConfigValueType: - """Return single configentry value for a core controller.""" - return cast( - ConfigValueType, - await self.client.send_command("config/core/get_value", domain=domain, key=key), - ) - - async def get_core_config_entries( - self, - domain: str, - action: str | None = None, - values: dict[str, ConfigValueType] | None = None, - ) -> tuple[ConfigEntry, ...]: - """ - Return Config entries to configure a core controller. - - core_controller: name of the core controller - action: [optional] action key called from config entries UI. - values: the (intermediate) raw values for config entries sent with the action. - """ - return tuple( - ConfigEntry.from_dict(x) - for x in await self.client.send_command( - "config/core/get_entries", - domain=domain, - action=action, - values=values, - ) - ) - - async def save_core_config( - self, - domain: str, - values: dict[str, ConfigValueType], - ) -> CoreConfig: - """Save CoreController Config values.""" - return CoreConfig.from_dict( - await self.client.send_command( - "config/core/get_entries", - domain=domain, - values=values, - ) - ) diff --git a/music_assistant/client/connection.py b/music_assistant/client/connection.py deleted file mode 100644 index 9d7597a24..000000000 --- a/music_assistant/client/connection.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Connect o a remote Music Assistant Server using the default Websocket API.""" - -from __future__ import annotations - -import logging -import pprint -from typing import Any, cast - -from aiohttp import ClientSession, ClientWebSocketResponse, WSMsgType, client_exceptions - -from music_assistant.client.exceptions import ( - CannotConnect, - ConnectionClosed, - ConnectionFailed, - InvalidMessage, - InvalidState, - NotConnected, -) -from music_assistant.common.helpers.json import json_dumps, json_loads - -LOGGER = logging.getLogger(f"{__package__}.connection") - - -def get_websocket_url(url: str) -> str: - """Extract Websocket URL from (base) Music Assistant URL.""" - if not url or "://" not in url: - msg = f"{url} is not a valid url" - raise RuntimeError(msg) - ws_url = url.replace("http", "ws") - if not ws_url.endswith("/ws"): - ws_url += "/ws" - return ws_url.replace("//ws", "/ws") - - -class WebsocketsConnection: - """Websockets connection to a Music Assistant Server.""" - - def __init__(self, server_url: str, aiohttp_session: ClientSession | None) -> None: - """Initialize.""" - self.ws_server_url = get_websocket_url(server_url) - self._aiohttp_session_provided = aiohttp_session is not None - self._aiohttp_session: ClientSession | None = aiohttp_session or ClientSession() - self._ws_client: ClientWebSocketResponse | None = None - - @property - def connected(self) -> bool: - """Return if we're currently connected.""" - return self._ws_client is not None and not self._ws_client.closed - - async def connect(self) -> dict[str, Any]: - """Connect to the websocket server and return the first message (server info).""" - if self._aiohttp_session is None: - self._aiohttp_session = ClientSession() - if self._ws_client is not None: - msg = "Already connected" - raise InvalidState(msg) - - LOGGER.debug("Trying to connect") - try: - self._ws_client = await self._aiohttp_session.ws_connect( - self.ws_server_url, - heartbeat=55, - compress=15, - max_msg_size=0, - ) - # receive first server info message - return await self.receive_message() - except ( - client_exceptions.WSServerHandshakeError, - client_exceptions.ClientError, - ) as err: - raise CannotConnect(err) from err - - async def disconnect(self) -> None: - """Disconnect the client.""" - LOGGER.debug("Closing client connection") - if self._ws_client is not None and not self._ws_client.closed: - await self._ws_client.close() - self._ws_client = None - if self._aiohttp_session and not self._aiohttp_session_provided: - await self._aiohttp_session.close() - self._aiohttp_session = None - - async def receive_message(self) -> dict[str, Any]: - """Receive the next message from the server (or raise on error).""" - assert self._ws_client - ws_msg = await self._ws_client.receive() - - if ws_msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.CLOSING): - raise ConnectionClosed("Connection was closed.") - - if ws_msg.type == WSMsgType.ERROR: - raise ConnectionFailed - - if ws_msg.type != WSMsgType.TEXT: - raise InvalidMessage(f"Received non-Text message: {ws_msg.type}") - - try: - msg = cast(dict[str, Any], json_loads(ws_msg.data)) - except TypeError as err: - raise InvalidMessage(f"Received unsupported JSON: {err}") from err - except ValueError as err: - raise InvalidMessage("Received invalid JSON.") from err - - if LOGGER.isEnabledFor(logging.DEBUG): - LOGGER.debug("Received message:\n%s\n", pprint.pformat(ws_msg)) - - return msg - - async def send_message(self, message: dict[str, Any]) -> None: - """ - Send a message to the server. - - Raises NotConnected if client not connected. - """ - if not self.connected: - raise NotConnected - - if LOGGER.isEnabledFor(logging.DEBUG): - LOGGER.debug("Publishing message:\n%s\n", pprint.pformat(message)) - - assert self._ws_client - assert isinstance(message, dict) - - await self._ws_client.send_json(message, dumps=json_dumps) - - def __repr__(self) -> str: - """Return the representation.""" - prefix = "" if self.connected else "not " - return f"{type(self).__name__}(ws_server_url={self.ws_server_url!r}, {prefix}connected)" diff --git a/music_assistant/client/exceptions.py b/music_assistant/client/exceptions.py deleted file mode 100644 index fb1349c31..000000000 --- a/music_assistant/client/exceptions.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Client-specific Exceptions for Music Assistant.""" - -from __future__ import annotations - - -class MusicAssistantClientException(Exception): - """Generic MusicAssistant exception.""" - - -class TransportError(MusicAssistantClientException): - """Exception raised to represent transport errors.""" - - def __init__(self, message: str, error: Exception | None = None) -> None: - """Initialize a transport error.""" - super().__init__(message) - self.error = error - - -class ConnectionClosed(TransportError): - """Exception raised when the connection is closed.""" - - -class CannotConnect(TransportError): - """Exception raised when failed to connect the client.""" - - def __init__(self, error: Exception) -> None: - """Initialize a cannot connect error.""" - super().__init__(f"{error}", error) - - -class ConnectionFailed(TransportError): - """Exception raised when an established connection fails.""" - - def __init__(self, error: Exception | None = None) -> None: - """Initialize a connection failed error.""" - if error is None: - super().__init__("Connection failed.") - return - super().__init__(f"{error}", error) - - -class NotConnected(MusicAssistantClientException): - """Exception raised when not connected to client.""" - - -class InvalidState(MusicAssistantClientException): - """Exception raised when data gets in invalid state.""" - - -class InvalidMessage(MusicAssistantClientException): - """Exception raised when an invalid message is received.""" - - -class InvalidServerVersion(MusicAssistantClientException): - """Exception raised when connected to server with incompatible version.""" diff --git a/music_assistant/client/music.py b/music_assistant/client/music.py deleted file mode 100644 index 797c10a59..000000000 --- a/music_assistant/client/music.py +++ /dev/null @@ -1,570 +0,0 @@ -"""Handle Music/library related endpoints for Music Assistant.""" - -from __future__ import annotations - -import urllib.parse -from typing import TYPE_CHECKING, cast - -from music_assistant.common.models.enums import AlbumType, ImageType, MediaType -from music_assistant.common.models.media_items import ( - Album, - Artist, - ItemMapping, - MediaItemImage, - MediaItemMetadata, - MediaItemType, - Playlist, - PlaylistTrack, - Radio, - SearchResults, - Track, - media_from_dict, -) -from music_assistant.common.models.provider import SyncTask -from music_assistant.common.models.queue_item import QueueItem - -if TYPE_CHECKING: - from .client import MusicAssistantClient - - -class Music: - """Music(library) related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - - # Tracks related endpoints/commands - - async def get_library_tracks( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - ) -> list[Track]: - """Get Track listing from the server.""" - return [ - Track.from_dict(obj) - for obj in await self.client.send_command( - "music/tracks/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - ) - ] - - async def get_track( - self, - item_id: str, - provider_instance_id_or_domain: str, - album_uri: str | None = None, - ) -> Track: - """Get single Track from the server.""" - return Track.from_dict( - await self.client.send_command( - "music/tracks/get_track", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - album_uri=album_uri, - ), - ) - - async def get_track_versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Track]: - """Get all other versions for given Track from the server.""" - return [ - Track.from_dict(item) - for item in await self.client.send_command( - "music/tracks/track_versions", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ] - - async def get_track_albums( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Album]: - """Get all (known) albums this track is featured on.""" - return [ - Album.from_dict(item) - for item in await self.client.send_command( - "music/tracks/track_albums", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - def get_track_preview_url( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> str: - """Get URL to preview clip of given track.""" - assert self.client.server_info - encoded_url = urllib.parse.quote(urllib.parse.quote(item_id)) - return f"{self.client.server_info.base_url}/preview?path={encoded_url}&provider={provider_instance_id_or_domain}" # noqa: E501 - - # Albums related endpoints/commands - - async def get_library_albums( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - album_types: list[AlbumType] | None = None, - ) -> list[Album]: - """Get Albums listing from the server.""" - return [ - Album.from_dict(obj) - for obj in await self.client.send_command( - "music/albums/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - album_types=album_types, - ) - ] - - async def get_album( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Album: - """Get single Album from the server.""" - return Album.from_dict( - await self.client.send_command( - "music/albums/get_album", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_album_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Track]: - """Get tracks for given album.""" - return [ - Track.from_dict(item) - for item in await self.client.send_command( - "music/albums/album_tracks", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - async def get_album_versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Album]: - """Get all other versions for given Album from the server.""" - return [ - Album.from_dict(item) - for item in await self.client.send_command( - "music/albums/album_versions", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ] - - # Artist related endpoints/commands - - async def get_library_artists( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - album_artists_only: bool = False, - ) -> list[Artist]: - """Get Artists listing from the server.""" - return [ - Artist.from_dict(obj) - for obj in await self.client.send_command( - "music/artists/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - album_artists_only=album_artists_only, - ) - ] - - async def get_artist( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Artist: - """Get single Artist from the server.""" - return Artist.from_dict( - await self.client.send_command( - "music/artists/get_artist", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_artist_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Track]: - """Get (top)tracks for given artist.""" - return [ - Track.from_dict(item) - for item in await self.client.send_command( - "music/artists/artist_tracks", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - async def get_artist_albums( - self, - item_id: str, - provider_instance_id_or_domain: str, - in_library_only: bool = False, - ) -> list[Album]: - """Get (top)albums for given artist.""" - return [ - Album.from_dict(item) - for item in await self.client.send_command( - "music/artists/artist_albums", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - in_library_only=in_library_only, - ) - ] - - # Playlist related endpoints/commands - - async def get_library_playlists( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - ) -> list[Playlist]: - """Get Playlists listing from the server.""" - return [ - Playlist.from_dict(obj) - for obj in await self.client.send_command( - "music/playlists/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - ) - ] - - async def get_playlist( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Playlist: - """Get single Playlist from the server.""" - return Playlist.from_dict( - await self.client.send_command( - "music/playlists/get_playlist", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_playlist_tracks( - self, - item_id: str, - provider_instance_id_or_domain: str, - page: int = 0, - ) -> list[PlaylistTrack]: - """Get tracks for given playlist.""" - return [ - PlaylistTrack.from_dict(obj) - for obj in await self.client.send_command( - "music/playlists/playlist_tracks", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - page=page, - ) - ] - - async def add_playlist_tracks(self, db_playlist_id: str | int, uris: list[str]) -> None: - """Add multiple tracks to playlist. Creates background tasks to process the action.""" - await self.client.send_command( - "music/playlists/add_playlist_tracks", - db_playlist_id=db_playlist_id, - uris=uris, - ) - - async def remove_playlist_tracks( - self, db_playlist_id: str | int, positions_to_remove: tuple[int, ...] - ) -> None: - """Remove multiple tracks from playlist.""" - await self.client.send_command( - "music/playlists/remove_playlist_tracks", - db_playlist_id=db_playlist_id, - positions_to_remove=positions_to_remove, - ) - - async def create_playlist( - self, name: str, provider_instance_or_domain: str | None = None - ) -> Playlist: - """Create new playlist.""" - return Playlist.from_dict( - await self.client.send_command( - "music/playlists/create_playlist", - name=name, - provider_instance_or_domain=provider_instance_or_domain, - ) - ) - - # Radio related endpoints/commands - - async def get_library_radios( - self, - favorite: bool | None = None, - search: str | None = None, - limit: int | None = None, - offset: int | None = None, - order_by: str | None = None, - ) -> list[Radio]: - """Get Radio listing from the server.""" - return [ - Radio.from_dict(obj) - for obj in await self.client.send_command( - "music/radios/library_items", - favorite=favorite, - search=search, - limit=limit, - offset=offset, - order_by=order_by, - ) - ] - - async def get_radio( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> Radio: - """Get single Radio from the server.""" - return Radio.from_dict( - await self.client.send_command( - "music/radios/get_radio", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ), - ) - - async def get_radio_versions( - self, - item_id: str, - provider_instance_id_or_domain: str, - ) -> list[Radio]: - """Get all other versions for given Radio from the server.""" - return [ - Radio.from_dict(item) - for item in await self.client.send_command( - "music/radios/radio_versions", - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ] - - # Other/generic endpoints/commands - - async def start_sync( - self, - media_types: list[MediaType] | None = None, - providers: list[str] | None = None, - ) -> None: - """Start running the sync of (all or selected) musicproviders. - - media_types: only sync these media types. None for all. - providers: only sync these provider instances. None for all. - """ - await self.client.send_command("music/sync", media_types=media_types, providers=providers) - - async def get_running_sync_tasks(self) -> list[SyncTask]: - """Return list with providers that are currently (scheduled for) syncing.""" - return [SyncTask(**item) for item in await self.client.send_command("music/synctasks")] - - async def search( - self, - search_query: str, - media_types: list[MediaType] = MediaType.ALL, - limit: int = 50, - library_only: bool = False, - ) -> SearchResults: - """Perform global search for media items on all providers. - - :param search_query: Search query. - :param media_types: A list of media_types to include. - :param limit: number of items to return in the search (per type). - """ - return SearchResults.from_dict( - await self.client.send_command( - "music/search", - search_query=search_query, - media_types=media_types, - limit=limit, - library_only=library_only, - ), - ) - - async def browse( - self, - path: str | None = None, - ) -> list[MediaItemType | ItemMapping]: - """Browse Music providers.""" - return [ - media_from_dict(obj) - for obj in await self.client.send_command("music/browse", path=path) - ] - - async def recently_played( - self, limit: int = 10, media_types: list[MediaType] | None = None - ) -> list[MediaItemType | ItemMapping]: - """Return a list of the last played items.""" - return [ - media_from_dict(item) - for item in await self.client.send_command( - "music/recently_played_items", limit=limit, media_types=media_types - ) - ] - - async def get_item_by_uri( - self, - uri: str, - ) -> MediaItemType | ItemMapping: - """Get single music item providing a mediaitem uri.""" - return media_from_dict(await self.client.send_command("music/item_by_uri", uri=uri)) - - async def get_item( - self, - media_type: MediaType, - item_id: str, - provider_instance_id_or_domain: str, - ) -> MediaItemType | ItemMapping: - """Get single music item by id and media type.""" - return media_from_dict( - await self.client.send_command( - "music/item", - media_type=media_type, - item_id=item_id, - provider_instance_id_or_domain=provider_instance_id_or_domain, - ) - ) - - async def add_item_to_favorites( - self, - item: str | MediaItemType, - ) -> None: - """Add an item to the favorites.""" - await self.client.send_command("music/favorites/add_item", item=item) - - async def remove_item_from_favorites( - self, - media_type: MediaType, - item_id: str | int, - ) -> None: - """Remove (library) item from the favorites.""" - await self.client.send_command( - "music/favorites/remove_item", - media_type=media_type, - item_id=item_id, - ) - - async def remove_item_from_library( - self, media_type: MediaType, library_item_id: str | int - ) -> None: - """ - Remove item from the library. - - Destructive! Will remove the item and all dependants. - """ - await self.client.send_command( - "music/library/remove_item", - media_type=media_type, - library_item_id=library_item_id, - ) - - async def add_item_to_library( - self, item: str | MediaItemType, overwrite_existing: bool = False - ) -> MediaItemType: - """Add item (uri or mediaitem) to the library.""" - return cast( - MediaItemType, - await self.client.send_command( - "music/library/add_item", item=item, overwrite_existing=overwrite_existing - ), - ) - - async def refresh_item( - self, - media_item: MediaItemType, - ) -> MediaItemType | ItemMapping | None: - """Try to refresh a mediaitem by requesting it's full object or search for substitutes.""" - if result := await self.client.send_command("music/refresh_item", media_item=media_item): - return media_from_dict(result) - return None - - # helpers - - def get_media_item_image( - self, - item: MediaItemType | ItemMapping | QueueItem, - type: ImageType = ImageType.THUMB, # noqa: A002 - ) -> MediaItemImage | None: - """Get MediaItemImage for MediaItem, ItemMapping.""" - if not item: - # guard for unexpected bad things - return None - # handle image in itemmapping - if item.image and item.image.type == type: - return item.image - # always prefer album image for tracks - album: Album | ItemMapping | None - if album := getattr(item, "album", None): - if album_image := self.get_media_item_image(album, type): - return album_image - # handle regular image within mediaitem - metadata: MediaItemMetadata | None - if metadata := getattr(item, "metadata", None): - for img in metadata.images or []: - if img.type == type: - return cast(MediaItemImage, img) - # retry with album/track artist(s) - artists: list[Artist | ItemMapping] | None - if artists := getattr(item, "artists", None): - for artist in artists: - if artist_image := self.get_media_item_image(artist, type): - return artist_image - # allow landscape fallback - if type == ImageType.THUMB: - return self.get_media_item_image(item, ImageType.LANDSCAPE) - return None diff --git a/music_assistant/client/player_queues.py b/music_assistant/client/player_queues.py deleted file mode 100644 index a52bf692f..000000000 --- a/music_assistant/client/player_queues.py +++ /dev/null @@ -1,251 +0,0 @@ -"""Handle PlayerQueues related endpoints for Music Assistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import EventType, QueueOption, RepeatMode -from music_assistant.common.models.player_queue import PlayerQueue -from music_assistant.common.models.queue_item import QueueItem - -if TYPE_CHECKING: - from collections.abc import Iterator - - from music_assistant.common.models.event import MassEvent - from music_assistant.common.models.media_items import MediaItemType - - from .client import MusicAssistantClient - - -class PlayerQueues: - """PlayerQueue related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - # subscribe to player events - client.subscribe( - self._handle_event, - ( - EventType.QUEUE_ADDED, - EventType.QUEUE_UPDATED, - ), - ) - # the initial items are retrieved after connect - self._queues: dict[str, PlayerQueue] = {} - - @property - def player_queues(self) -> list[PlayerQueue]: - """Return all player queues.""" - return list(self._queues.values()) - - def __iter__(self) -> Iterator[PlayerQueue]: - """Iterate over (available) PlayerQueues.""" - return iter(self._queues.values()) - - def get(self, queue_id: str) -> PlayerQueue | None: - """Return PlayerQueue by ID (or None if not found).""" - return self._queues.get(queue_id) - - # PlayerQueue related endpoints/commands - - async def get_player_queue_items( - self, queue_id: str, limit: int = 500, offset: int = 0 - ) -> list[QueueItem]: - """Get all QueueItems for given PlayerQueue.""" - return [ - QueueItem.from_dict(obj) - for obj in await self.client.send_command( - "player_queues/items", queue_id=queue_id, limit=limit, offset=offset - ) - ] - - async def get_active_queue(self, player_id: str) -> PlayerQueue: - """Return the current active/synced queue for a player.""" - return PlayerQueue.from_dict( - await self.client.send_command("player_queues/get_active_queue", player_id=player_id) - ) - - async def queue_command_play(self, queue_id: str) -> None: - """Send PLAY command to given queue.""" - await self.client.send_command("player_queues/play", queue_id=queue_id) - - async def queue_command_pause(self, queue_id: str) -> None: - """Send PAUSE command to given queue.""" - await self.client.send_command("player_queues/pause", queue_id=queue_id) - - async def queue_command_stop(self, queue_id: str) -> None: - """Send STOP command to given queue.""" - await self.client.send_command("player_queues/stop", queue_id=queue_id) - - async def queue_command_resume(self, queue_id: str, fade_in: bool | None = None) -> None: - """Handle RESUME command for given queue. - - - queue_id: queue_id of the queue to handle the command. - """ - await self.client.send_command("player_queues/resume", queue_id=queue_id, fade_in=fade_in) - - async def queue_command_next(self, queue_id: str) -> None: - """Send NEXT TRACK command to given queue.""" - await self.client.send_command("player_queues/next", queue_id=queue_id) - - async def queue_command_previous(self, queue_id: str) -> None: - """Send PREVIOUS TRACK command to given queue.""" - await self.client.send_command("player_queues/previous", queue_id=queue_id) - - async def queue_command_clear(self, queue_id: str) -> None: - """Send CLEAR QUEUE command to given queue.""" - await self.client.send_command("player_queues/clear", queue_id=queue_id) - - async def queue_command_move_item( - self, queue_id: str, queue_item_id: str, pos_shift: int = 1 - ) -> None: - """ - Move queue item x up/down the queue. - - Parameters: - - queue_id: id of the queue to process this request. - - queue_item_id: the item_id of the queueitem that needs to be moved. - - pos_shift: move item x positions down if positive value - - pos_shift: move item x positions up if negative value - - pos_shift: move item to top of queue as next item if 0 - - NOTE: Fails if the given QueueItem is already playing or loaded in the buffer. - """ - await self.client.send_command( - "player_queues/move_item", - queue_id=queue_id, - queue_item_id=queue_item_id, - pos_shift=pos_shift, - ) - - async def queue_command_move_up(self, queue_id: str, queue_item_id: str) -> None: - """Move given queue item one place up in the queue.""" - await self.queue_command_move_item( - queue_id=queue_id, queue_item_id=queue_item_id, pos_shift=-1 - ) - - async def queue_command_move_down(self, queue_id: str, queue_item_id: str) -> None: - """Move given queue item one place down in the queue.""" - await self.queue_command_move_item( - queue_id=queue_id, queue_item_id=queue_item_id, pos_shift=1 - ) - - async def queue_command_move_next(self, queue_id: str, queue_item_id: str) -> None: - """Move given queue item as next up in the queue.""" - await self.queue_command_move_item( - queue_id=queue_id, queue_item_id=queue_item_id, pos_shift=0 - ) - - async def queue_command_delete(self, queue_id: str, item_id_or_index: int | str) -> None: - """Delete item (by id or index) from the queue.""" - await self.client.send_command( - "player_queues/delete_item", queue_id=queue_id, item_id_or_index=item_id_or_index - ) - - async def queue_command_seek(self, queue_id: str, position: int) -> None: - """ - Handle SEEK command for given queue. - - Parameters: - - position: position in seconds to seek to in the current playing item. - """ - await self.client.send_command("player_queues/seek", queue_id=queue_id, position=position) - - async def queue_command_skip(self, queue_id: str, seconds: int) -> None: - """ - Handle SKIP command for given queue. - - Parameters: - - seconds: number of seconds to skip in track. Use negative value to skip back. - """ - await self.client.send_command("player_queues/skip", queue_id=queue_id, seconds=seconds) - - async def queue_command_shuffle(self, queue_id: str, shuffle_enabled: bool) -> None: - """Configure shuffle mode on the the queue.""" - await self.client.send_command( - "player_queues/shuffle", queue_id=queue_id, shuffle_enabled=shuffle_enabled - ) - - async def queue_command_repeat(self, queue_id: str, repeat_mode: RepeatMode) -> None: - """Configure repeat mode on the the queue.""" - await self.client.send_command( - "player_queues/repeat", queue_id=queue_id, repeat_mode=repeat_mode - ) - - async def play_index( - self, - queue_id: str, - index: int | str, - seek_position: int = 0, - fade_in: bool = False, - ) -> None: - """Play item at index (or item_id) X in queue.""" - await self.client.send_command( - "player_queues/repeat", - queue_id=queue_id, - index=index, - seek_position=seek_position, - fade_in=fade_in, - ) - - async def play_media( - self, - queue_id: str, - media: MediaItemType | list[MediaItemType] | str | list[str], - option: QueueOption | None = None, - radio_mode: bool = False, - start_item: str | None = None, - ) -> None: - """ - Play media item(s) on the given queue. - - - media: Media that should be played (MediaItem(s) or uri's). - - queue_opt: Which enqueue mode to use. - - radio_mode: Enable radio mode for the given item(s). - - start_item: Optional item to start the playlist or album from. - """ - await self.client.send_command( - "player_queues/play_media", - queue_id=queue_id, - media=media, - option=option, - radio_mode=radio_mode, - start_item=start_item, - ) - - async def transfer_queue( - self, - source_queue_id: str, - target_queue_id: str, - auto_play: bool | None = None, - ) -> None: - """Transfer queue to another queue.""" - await self.client.send_command( - "player_queues/transfer", - source_queue_id=source_queue_id, - target_queue_id=target_queue_id, - auto_play=auto_play, - require_schema=25, - ) - - # Other endpoints/commands - - async def _get_player_queues(self) -> list[PlayerQueue]: - """Fetch all PlayerQueues from the server.""" - return [ - PlayerQueue.from_dict(item) - for item in await self.client.send_command("player_queues/all") - ] - - async def fetch_state(self) -> None: - """Fetch initial state once the server is connected.""" - for queue in await self._get_player_queues(): - self._queues[queue.queue_id] = queue - - def _handle_event(self, event: MassEvent) -> None: - """Handle incoming player(queue) event.""" - if event.event in (EventType.QUEUE_ADDED, EventType.QUEUE_UPDATED): - # Queue events always have an object_id - assert event.object_id - self._queues[event.object_id] = PlayerQueue.from_dict(event.data) diff --git a/music_assistant/client/players.py b/music_assistant/client/players.py deleted file mode 100644 index 3173cabad..000000000 --- a/music_assistant/client/players.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Handle player related endpoints for Music Assistant.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.player import Player - -if TYPE_CHECKING: - from collections.abc import Iterator - - from music_assistant.common.models.event import MassEvent - - from .client import MusicAssistantClient - - -class Players: - """Player related endpoints/data for Music Assistant.""" - - def __init__(self, client: MusicAssistantClient) -> None: - """Handle Initialization.""" - self.client = client - # subscribe to player events - client.subscribe( - self._handle_event, - ( - EventType.PLAYER_ADDED, - EventType.PLAYER_REMOVED, - EventType.PLAYER_UPDATED, - ), - ) - # the initial items are retrieved after connect - self._players: dict[str, Player] = {} - - @property - def players(self) -> list[Player]: - """Return all players.""" - return list(self._players.values()) - - def __iter__(self) -> Iterator[Player]: - """Iterate over (available) players.""" - return iter(self._players.values()) - - def get(self, player_id: str) -> Player | None: - """Return Player by ID (or None if not found).""" - return self._players.get(player_id) - - def __getitem__(self, player_id: str) -> Player: - """Return Player by ID.""" - return self._players[player_id] - - # Player related endpoints/commands - - async def player_command_stop(self, player_id: str) -> None: - """Send STOP command to given player (directly).""" - await self.client.send_command("players/cmd/stop", player_id=player_id) - - async def player_command_play(self, player_id: str) -> None: - """Send PLAY command to given player (directly).""" - await self.client.send_command("players/cmd/play", player_id=player_id) - - async def player_command_pause(self, player_id: str) -> None: - """Send PAUSE command to given player (directly).""" - await self.client.send_command("players/cmd/pause", player_id=player_id) - - async def player_command_play_pause(self, player_id: str) -> None: - """Send PLAY_PAUSE (toggle) command to given player (directly).""" - await self.client.send_command("players/cmd/pause", player_id=player_id) - - async def player_command_power(self, player_id: str, powered: bool) -> None: - """Send POWER command to given player.""" - await self.client.send_command("players/cmd/power", player_id=player_id, powered=powered) - - async def player_command_volume_set(self, player_id: str, volume_level: int) -> None: - """Send VOLUME SET command to given player.""" - await self.client.send_command( - "players/cmd/volume_set", player_id=player_id, volume_level=volume_level - ) - - async def player_command_volume_up(self, player_id: str) -> None: - """Send VOLUME UP command to given player.""" - await self.client.send_command("players/cmd/volume_up", player_id=player_id) - - async def player_command_volume_down(self, player_id: str) -> None: - """Send VOLUME DOWN command to given player.""" - await self.client.send_command("players/cmd/volume_down", player_id=player_id) - - async def player_command_volume_mute(self, player_id: str, muted: bool) -> None: - """Send VOLUME MUTE command to given player.""" - await self.client.send_command("players/cmd/volume_mute", player_id=player_id, muted=muted) - - async def player_command_seek(self, player_id: str, position: int) -> None: - """Handle SEEK command for given player (directly). - - - player_id: player_id of the player to handle the command. - - position: position in seconds to seek to in the current playing item. - """ - await self.client.send_command("players/cmd/seek", player_id=player_id, position=position) - - async def player_command_sync(self, player_id: str, target_player: str) -> None: - """ - Handle SYNC command for given player. - - Join/add the given player(id) to the given (master) player/sync group. - If the player is already synced to another player, it will be unsynced there first. - If the target player itself is already synced to another player, this will fail. - If the player can not be synced with the given target player, this will fail. - - - player_id: player_id of the player to handle the command. - - target_player: player_id of the syncgroup master or group player. - """ - await self.client.send_command( - "players/cmd/sync", player_id=player_id, target_player=target_player - ) - - async def player_command_unsync(self, player_id: str) -> None: - """ - Handle UNSYNC command for given player. - - Remove the given player from any syncgroups it currently is synced to. - If the player is not currently synced to any other player, - this will silently be ignored. - - - player_id: player_id of the player to handle the command. - """ - await self.client.send_command("players/cmd/unsync", player_id=player_id) - - async def cmd_sync_many(self, target_player: str, child_player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" - await self.client.send_command( - "players/cmd/sync_many", target_player=target_player, child_player_ids=child_player_ids - ) - - async def cmd_unsync_many(self, player_ids: list[str]) -> None: - """Create temporary sync group by joining given players to target player.""" - await self.client.send_command("players/cmd/unsync_many", player_ids=player_ids) - - async def play_announcement( - self, - player_id: str, - url: str, - use_pre_announce: bool | None = None, - volume_level: int | None = None, - ) -> None: - """Handle playback of an announcement (url) on given player.""" - await self.client.send_command( - "players/cmd/play_announcement", - player_id=player_id, - url=url, - use_pre_announce=use_pre_announce, - volume_level=volume_level, - ) - - # PlayerGroup related endpoints/commands - - async def create_syncgroup(self, name: str, members: list[str]) -> Player: - """Create a new Sync Group with name and members. - - - name: Name for the new group to create. - - members: A list of player_id's that should be part of this group. - - Returns the newly created player on success. - """ - return Player.from_dict( - await self.client.send_command("players/create_syncgroup", name=name, members=members) - ) - - async def set_player_group_volume(self, player_id: str, volume_level: int) -> None: - """ - Send VOLUME_SET command to given playergroup. - - Will send the new (average) volume level to group child's. - - player_id: player_id of the playergroup to handle the command. - - volume_level: volume level (0..100) to set on the player. - """ - await self.client.send_command( - "players/cmd/group_volume", player_id=player_id, volume_level=volume_level - ) - - async def set_player_group_members(self, player_id: str, members: list[str]) -> None: - """ - Update the memberlist of the given PlayerGroup. - - - player_id: player_id of the groupplayer to handle the command. - - members: list of player ids to set as members. - """ - await self.client.send_command( - "players/cmd/set_members", player_id=player_id, members=members - ) - - # Other endpoints/commands - - async def _get_players(self) -> list[Player]: - """Fetch all Players from the server.""" - return [Player.from_dict(item) for item in await self.client.send_command("players/all")] - - async def fetch_state(self) -> None: - """Fetch initial state once the server is connected.""" - for player in await self._get_players(): - self._players[player.player_id] = player - - def _handle_event(self, event: MassEvent) -> None: - """Handle incoming player event.""" - if event.event in (EventType.PLAYER_ADDED, EventType.PLAYER_UPDATED): - # Player events always have an object id - assert event.object_id - self._players[event.object_id] = Player.from_dict(event.data) - return - if event.event == EventType.PLAYER_REMOVED: - # Player events always have an object id - assert event.object_id - self._players.pop(event.object_id, None) diff --git a/music_assistant/common/__init__.py b/music_assistant/common/__init__.py deleted file mode 100644 index c450cee23..000000000 --- a/music_assistant/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Provide common/shared files for the Music Assistant Server and client.""" diff --git a/music_assistant/common/helpers/__init__.py b/music_assistant/common/helpers/__init__.py deleted file mode 100644 index 65147294e..000000000 --- a/music_assistant/common/helpers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Various utils/helpers.""" diff --git a/music_assistant/common/models/__init__.py b/music_assistant/common/models/__init__.py deleted file mode 100644 index b10f70014..000000000 --- a/music_assistant/common/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Package with all common/shared (serializable) Models (dataclassses).""" diff --git a/music_assistant/common/models/api.py b/music_assistant/common/models/api.py deleted file mode 100644 index ec43ea02f..000000000 --- a/music_assistant/common/models/api.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Generic models used for the (websockets) API communication.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import Any - -from mashumaro.mixins.orjson import DataClassORJSONMixin - -from music_assistant.common.helpers.json import get_serializable_value -from music_assistant.common.models.event import MassEvent - - -@dataclass -class CommandMessage(DataClassORJSONMixin): - """Model for a Message holding a command from server to client or client to server.""" - - message_id: str | int - command: str - args: dict[str, Any] | None = None - - -@dataclass -class ResultMessageBase(DataClassORJSONMixin): - """Base class for a result/response of a Command Message.""" - - message_id: str - - -@dataclass -class SuccessResultMessage(ResultMessageBase): - """Message sent when a Command has been successfully executed.""" - - result: Any = field(default=None, metadata={"serialize": lambda v: get_serializable_value(v)}) - partial: bool = False - - -@dataclass -class ErrorResultMessage(ResultMessageBase): - """Message sent when a command did not execute successfully.""" - - error_code: int - details: str | None = None - - -# EventMessage is the same as MassEvent, this is just a alias. -EventMessage = MassEvent - - -@dataclass -class ServerInfoMessage(DataClassORJSONMixin): - """Message sent by the server with it's info when a client connects.""" - - server_id: str - server_version: str - schema_version: int - min_supported_schema_version: int - base_url: str - homeassistant_addon: bool = False - onboard_done: bool = False - - -MessageType = ( - CommandMessage | EventMessage | SuccessResultMessage | ErrorResultMessage | ServerInfoMessage -) - - -def parse_message(raw: dict[Any, Any]) -> MessageType: - """Parse Message from raw dict object.""" - if "event" in raw: - return EventMessage.from_dict(raw) - if "error_code" in raw: - return ErrorResultMessage.from_dict(raw) - if "result" in raw: - return SuccessResultMessage.from_dict(raw) - if "sdk_version" in raw: - return ServerInfoMessage.from_dict(raw) - return CommandMessage.from_dict(raw) diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py deleted file mode 100644 index 78480255f..000000000 --- a/music_assistant/common/models/config_entries.py +++ /dev/null @@ -1,677 +0,0 @@ -"""Model and helpers for Config entries.""" - -from __future__ import annotations - -import logging -import warnings -from collections.abc import Callable, Iterable -from dataclasses import dataclass -from enum import Enum -from types import NoneType -from typing import Any - -from mashumaro import DataClassDictMixin - -from music_assistant.common.models.enums import ProviderType -from music_assistant.constants import ( - CONF_ANNOUNCE_VOLUME, - CONF_ANNOUNCE_VOLUME_MAX, - CONF_ANNOUNCE_VOLUME_MIN, - CONF_ANNOUNCE_VOLUME_STRATEGY, - CONF_AUTO_PLAY, - CONF_CROSSFADE, - CONF_CROSSFADE_DURATION, - CONF_ENABLE_ICY_METADATA, - CONF_ENFORCE_MP3, - CONF_EQ_BASS, - CONF_EQ_MID, - CONF_EQ_TREBLE, - CONF_FLOW_MODE, - CONF_HIDE_PLAYER, - CONF_HTTP_PROFILE, - CONF_ICON, - CONF_LOG_LEVEL, - CONF_OUTPUT_CHANNELS, - CONF_SAMPLE_RATES, - CONF_SYNC_ADJUST, - CONF_TTS_PRE_ANNOUNCE, - CONF_VOLUME_NORMALIZATION, - CONF_VOLUME_NORMALIZATION_TARGET, - SECURE_STRING_SUBSTITUTE, -) - -from .enums import ConfigEntryType - -# TEMP: ignore UserWarnings from mashumaro -# https://github.com/Fatal1ty/mashumaro/issues/221 -warnings.filterwarnings("ignore", category=UserWarning, module="mashumaro") - -LOGGER = logging.getLogger(__name__) - -ENCRYPT_CALLBACK: Callable[[str], str] | None = None -DECRYPT_CALLBACK: Callable[[str], str] | None = None - -ConfigValueType = ( - str - | int - | float - | bool - | list[str] - | list[tuple[int, int]] - | tuple[int, int] - | list[int] - | Enum - | None -) - -ConfigEntryTypeMap: dict[ConfigEntryType, type[ConfigValueType]] = { - ConfigEntryType.BOOLEAN: bool, - ConfigEntryType.STRING: str, - ConfigEntryType.SECURE_STRING: str, - ConfigEntryType.INTEGER: int, - ConfigEntryType.INTEGER_TUPLE: tuple[int, int], - ConfigEntryType.FLOAT: float, - ConfigEntryType.LABEL: str, - ConfigEntryType.DIVIDER: str, - ConfigEntryType.ACTION: str, - ConfigEntryType.ALERT: str, - ConfigEntryType.ICON: str, -} - -UI_ONLY = ( - ConfigEntryType.LABEL, - ConfigEntryType.DIVIDER, - ConfigEntryType.ACTION, - ConfigEntryType.ALERT, -) - - -@dataclass -class ConfigValueOption(DataClassDictMixin): - """Model for a value with separated name/value.""" - - title: str - value: ConfigValueType - - -@dataclass -class ConfigEntry(DataClassDictMixin): - """Model for a Config Entry. - - The definition of something that can be configured - for an object (e.g. provider or player) - within Music Assistant. - """ - - # key: used as identifier for the entry, also for localization - key: str - type: ConfigEntryType - # label: default label when no translation for the key is present - label: str - default_value: ConfigValueType = None - required: bool = True - # options [optional]: select from list of possible values/options - options: tuple[ConfigValueOption, ...] | None = None - # range [optional]: select values within range - range: tuple[int, int] | None = None - # description [optional]: extended description of the setting. - description: str | None = None - # help_link [optional]: link to help article. - help_link: str | None = None - # multi_value [optional]: allow multiple values from the list - multi_value: bool = False - # depends_on [optional]: needs to be set before this setting shows up in frontend - depends_on: str | None = None - # hidden: hide from UI - hidden: bool = False - # category: category to group this setting into in the frontend (e.g. advanced) - category: str = "generic" - # action: (configentry)action that is needed to get the value for this entry - action: str | None = None - # action_label: default label for the action when no translation for the action is present - action_label: str | None = None - # value: set by the config manager/flow (or in rare cases by the provider itself) - value: ConfigValueType = None - - def parse_value( - self, - value: ConfigValueType, - allow_none: bool = True, - ) -> ConfigValueType: - """Parse value from the config entry details and plain value.""" - expected_type = list if self.multi_value else ConfigEntryTypeMap.get(self.type, NoneType) - if value is None: - value = self.default_value - if value is None and (not self.required or allow_none): - expected_type = NoneType - if self.type == ConfigEntryType.LABEL: - value = self.label - if not isinstance(value, expected_type): - # handle common conversions/mistakes - if expected_type is float and isinstance(value, int): - self.value = float(value) - return self.value - if expected_type is int and isinstance(value, float): - self.value = int(value) - return self.value - for val_type in (int, float): - # convert int/float from string - if expected_type == val_type and isinstance(value, str): - try: - self.value = val_type(value) - return self.value - except ValueError: - pass - if self.type in UI_ONLY: - self.value = self.default_value - return self.value - # fallback to default - if self.default_value is not None: - LOGGER.warning( - "%s has unexpected type: %s, fallback to default", - self.key, - type(self.value), - ) - value = self.default_value - if not (value is None and allow_none): - msg = f"{self.key} has unexpected type: {type(value)}" - raise ValueError(msg) - self.value = value - return self.value - - -@dataclass -class Config(DataClassDictMixin): - """Base Configuration object.""" - - values: dict[str, ConfigEntry] - - def get_value(self, key: str) -> ConfigValueType: - """Return config value for given key.""" - config_value = self.values[key] - if config_value.type == ConfigEntryType.SECURE_STRING and config_value.value: - assert isinstance(config_value.value, str) - assert DECRYPT_CALLBACK is not None - return DECRYPT_CALLBACK(config_value.value) - return config_value.value - - @classmethod - def parse( - cls, - config_entries: Iterable[ConfigEntry], - raw: dict[str, Any], - ) -> Config: - """Parse Config from the raw values (as stored in persistent storage).""" - conf = cls.from_dict({**raw, "values": {}}) - for entry in config_entries: - # unpack Enum value in default_value - if isinstance(entry.default_value, Enum): - entry.default_value = entry.default_value.value - # create a copy of the entry - conf.values[entry.key] = ConfigEntry.from_dict(entry.to_dict()) - conf.values[entry.key].parse_value( - raw.get("values", {}).get(entry.key), allow_none=True - ) - return conf - - def to_raw(self) -> dict[str, Any]: - """Return minimized/raw dict to store in persistent storage.""" - - def _handle_value(value: ConfigEntry) -> ConfigValueType: - if value.type == ConfigEntryType.SECURE_STRING: - assert isinstance(value.value, str) - assert ENCRYPT_CALLBACK is not None - return ENCRYPT_CALLBACK(value.value) - return value.value - - res = self.to_dict() - res["values"] = { - x.key: _handle_value(x) - for x in self.values.values() - if (x.value != x.default_value and x.type not in UI_ONLY) - } - return res - - def __post_serialize__(self, d: dict[str, Any]) -> dict[str, Any]: - """Adjust dict object after it has been serialized.""" - for key, value in self.values.items(): - # drop all password values from the serialized dict - # API consumers (including the frontend) are not allowed to retrieve it - # (even if its encrypted) but they can only set it. - if value.value and value.type == ConfigEntryType.SECURE_STRING: - d["values"][key]["value"] = SECURE_STRING_SUBSTITUTE - return d - - def update(self, update: dict[str, ConfigValueType]) -> set[str]: - """Update Config with updated values.""" - changed_keys: set[str] = set() - - # root values (enabled, name) - root_values = ("enabled", "name") - for key in root_values: - if key not in update: - continue - cur_val = getattr(self, key) - new_val = update[key] - if new_val == cur_val: - continue - setattr(self, key, new_val) - changed_keys.add(key) - - for key, new_val in update.items(): - if key in root_values: - continue - if key not in self.values: - continue - cur_val = self.values[key].value if key in self.values else None - # parse entry to do type validation - parsed_val = self.values[key].parse_value(new_val) - if cur_val != parsed_val: - changed_keys.add(f"values/{key}") - - return changed_keys - - def validate(self) -> None: - """Validate if configuration is valid.""" - # For now we just use the parse method to check for not allowed None values - # this can be extended later - for value in self.values.values(): - value.parse_value(value.value, allow_none=False) - - -@dataclass -class ProviderConfig(Config): - """Provider(instance) Configuration.""" - - type: ProviderType - domain: str - instance_id: str - # enabled: boolean to indicate if the provider is enabled - enabled: bool = True - # name: an (optional) custom name for this provider instance/config - name: str | None = None - # last_error: an optional error message if the provider could not be setup with this config - last_error: str | None = None - - -@dataclass -class PlayerConfig(Config): - """Player Configuration.""" - - provider: str - player_id: str - # enabled: boolean to indicate if the player is enabled - enabled: bool = True - # name: an (optional) custom name for this player - name: str | None = None - # available: boolean to indicate if the player is available - available: bool = True - # default_name: default name to use when there is no name available - default_name: str | None = None - - -@dataclass -class CoreConfig(Config): - """CoreController Configuration.""" - - domain: str # domain/name of the core module - # last_error: an optional error message if the module could not be setup with this config - last_error: str | None = None - - -CONF_ENTRY_LOG_LEVEL = ConfigEntry( - key=CONF_LOG_LEVEL, - type=ConfigEntryType.STRING, - label="Log level", - options=( - ConfigValueOption("global", "GLOBAL"), - ConfigValueOption("info", "INFO"), - ConfigValueOption("warning", "WARNING"), - ConfigValueOption("error", "ERROR"), - ConfigValueOption("debug", "DEBUG"), - ConfigValueOption("verbose", "VERBOSE"), - ), - default_value="GLOBAL", - category="advanced", -) - -DEFAULT_PROVIDER_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) -DEFAULT_CORE_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) - -# some reusable player config entries - -CONF_ENTRY_FLOW_MODE = ConfigEntry( - key=CONF_FLOW_MODE, - type=ConfigEntryType.BOOLEAN, - label="Enable queue flow mode", - default_value=False, -) - -CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED = ConfigEntry.from_dict( - {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True} -) - -CONF_ENTRY_FLOW_MODE_ENFORCED = ConfigEntry.from_dict( - {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True, "value": True, "hidden": True} -) - -CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED = ConfigEntry.from_dict( - {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": False, "value": False, "hidden": True} -) - - -CONF_ENTRY_AUTO_PLAY = ConfigEntry( - key=CONF_AUTO_PLAY, - type=ConfigEntryType.BOOLEAN, - label="Automatically play/resume on power on", - default_value=False, - description="When this player is turned ON, automatically start playing " - "(if there are items in the queue).", -) - -CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry( - key=CONF_OUTPUT_CHANNELS, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Stereo (both channels)", "stereo"), - ConfigValueOption("Left channel", "left"), - ConfigValueOption("Right channel", "right"), - ConfigValueOption("Mono (both channels)", "mono"), - ), - default_value="stereo", - label="Output Channel Mode", - category="audio", -) - -CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry( - key=CONF_VOLUME_NORMALIZATION, - type=ConfigEntryType.BOOLEAN, - label="Enable volume normalization", - default_value=True, - description="Enable volume normalization (EBU-R128 based)", - category="audio", -) - -CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry( - key=CONF_VOLUME_NORMALIZATION_TARGET, - type=ConfigEntryType.INTEGER, - range=(-70, -5), - default_value=-17, - label="Target level for volume normalization", - description="Adjust average (perceived) loudness to this target level", - depends_on=CONF_VOLUME_NORMALIZATION, - category="advanced", -) - -CONF_ENTRY_EQ_BASS = ConfigEntry( - key=CONF_EQ_BASS, - type=ConfigEntryType.INTEGER, - range=(-10, 10), - default_value=0, - label="Equalizer: bass", - description="Use the builtin basic equalizer to adjust the bass of audio.", - category="audio", -) - -CONF_ENTRY_EQ_MID = ConfigEntry( - key=CONF_EQ_MID, - type=ConfigEntryType.INTEGER, - range=(-10, 10), - default_value=0, - label="Equalizer: midrange", - description="Use the builtin basic equalizer to adjust the midrange of audio.", - category="audio", -) - -CONF_ENTRY_EQ_TREBLE = ConfigEntry( - key=CONF_EQ_TREBLE, - type=ConfigEntryType.INTEGER, - range=(-10, 10), - default_value=0, - label="Equalizer: treble", - description="Use the builtin basic equalizer to adjust the treble of audio.", - category="audio", -) - - -CONF_ENTRY_CROSSFADE = ConfigEntry( - key=CONF_CROSSFADE, - type=ConfigEntryType.BOOLEAN, - label="Enable crossfade", - default_value=False, - description="Enable a crossfade transition between (queue) tracks.", - category="audio", -) - -CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED = ConfigEntry( - key=CONF_CROSSFADE, - type=ConfigEntryType.BOOLEAN, - label="Enable crossfade", - default_value=False, - description="Enable a crossfade transition between (queue) tracks.\n\n " - "Requires flow-mode to be enabled", - category="audio", - depends_on=CONF_FLOW_MODE, -) - -CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry( - key=CONF_CROSSFADE_DURATION, - type=ConfigEntryType.INTEGER, - range=(1, 10), - default_value=8, - label="Crossfade duration", - description="Duration in seconds of the crossfade between tracks (if enabled)", - depends_on=CONF_CROSSFADE, - category="advanced", -) - -CONF_ENTRY_HIDE_PLAYER = ConfigEntry( - key=CONF_HIDE_PLAYER, - type=ConfigEntryType.BOOLEAN, - label="Hide this player in the user interface", - default_value=False, -) - -CONF_ENTRY_ENFORCE_MP3 = ConfigEntry( - key=CONF_ENFORCE_MP3, - type=ConfigEntryType.BOOLEAN, - label="Enforce (lossy) mp3 stream", - default_value=False, - description="By default, Music Assistant sends lossless, high quality audio " - "to all players. Some players can not deal with that and require the stream to be packed " - "into a lossy mp3 codec. \n\n " - "Only enable when needed. Saves some bandwidth at the cost of audio quality.", - category="audio", -) - -CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED = ConfigEntry.from_dict( - {**CONF_ENTRY_ENFORCE_MP3.to_dict(), "default_value": True} -) - -CONF_ENTRY_SYNC_ADJUST = ConfigEntry( - key=CONF_SYNC_ADJUST, - type=ConfigEntryType.INTEGER, - range=(-500, 500), - default_value=0, - label="Audio synchronization delay correction", - description="If this player is playing audio synced with other players " - "and you always hear the audio too early or late on this player, " - "you can shift the audio a bit.", - category="advanced", -) - - -CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry( - key=CONF_TTS_PRE_ANNOUNCE, - type=ConfigEntryType.BOOLEAN, - default_value=True, - label="Pre-announce TTS announcements", - category="announcements", -) - - -CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME_STRATEGY, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Absolute volume", "absolute"), - ConfigValueOption("Relative volume increase", "relative"), - ConfigValueOption("Volume increase by fixed percentage", "percentual"), - ConfigValueOption("Do not adjust volume", "none"), - ), - default_value="percentual", - label="Volume strategy for Announcements", - category="announcements", -) - -CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME, - type=ConfigEntryType.INTEGER, - default_value=85, - label="Volume for Announcements", - category="announcements", -) - -CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME_MIN, - type=ConfigEntryType.INTEGER, - default_value=15, - label="Minimum Volume level for Announcements", - description="The volume (adjustment) of announcements should no go below this level.", - category="announcements", -) - -CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry( - key=CONF_ANNOUNCE_VOLUME_MAX, - type=ConfigEntryType.INTEGER, - default_value=75, - label="Maximum Volume level for Announcements", - description="The volume (adjustment) of announcements should no go above this level.", - category="announcements", -) - -CONF_ENTRY_PLAYER_ICON = ConfigEntry( - key=CONF_ICON, - type=ConfigEntryType.ICON, - default_value="mdi-speaker", - label="Icon", - description="Material design icon for this player. " - "\n\nSee https://pictogrammers.com/library/mdi/", - category="generic", -) - -CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict( - {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"} -) - -CONF_ENTRY_SAMPLE_RATES = ConfigEntry( - key=CONF_SAMPLE_RATES, - type=ConfigEntryType.INTEGER_TUPLE, - options=( - ConfigValueOption("44.1kHz / 16 bits", (44100, 16)), - ConfigValueOption("44.1kHz / 24 bits", (44100, 24)), - ConfigValueOption("48kHz / 16 bits", (48000, 16)), - ConfigValueOption("48kHz / 24 bits", (48000, 24)), - ConfigValueOption("88.2kHz / 16 bits", (88200, 16)), - ConfigValueOption("88.2kHz / 24 bits", (88200, 24)), - ConfigValueOption("96kHz / 16 bits", (96000, 16)), - ConfigValueOption("96kHz / 24 bits", (96000, 24)), - ConfigValueOption("176.4kHz / 16 bits", (176400, 16)), - ConfigValueOption("176.4kHz / 24 bits", (176400, 24)), - ConfigValueOption("192kHz / 16 bits", (192000, 16)), - ConfigValueOption("192kHz / 24 bits", (192000, 24)), - ConfigValueOption("352.8kHz / 16 bits", (352800, 16)), - ConfigValueOption("352.8kHz / 24 bits", (352800, 24)), - ConfigValueOption("384kHz / 16 bits", (384000, 16)), - ConfigValueOption("384kHz / 24 bits", (384000, 24)), - ), - default_value=[(44100, 16), (48000, 16)], - required=True, - multi_value=True, - label="Sample rates supported by this player", - category="advanced", - description="The sample rates (and bit depths) supported by this player.\n" - "Content with unsupported sample rates will be automatically resampled.", -) - - -CONF_ENTRY_HTTP_PROFILE = ConfigEntry( - key=CONF_HTTP_PROFILE, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Profile 1 - chunked", "chunked"), - ConfigValueOption("Profile 2 - no content length", "no_content_length"), - ConfigValueOption("Profile 3 - forced content length", "forced_content_length"), - ), - default_value="no_content_length", - label="HTTP Profile used for sending audio", - category="advanced", - description="This is considered to be a very advanced setting, only adjust this if needed, " - "for example if your player stops playing halfway streams or if you experience " - "other playback related issues. In most cases the default setting is fine.", -) - -CONF_ENTRY_HTTP_PROFILE_FORCED_1 = ConfigEntry.from_dict( - {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "chunked", "hidden": True} -) -CONF_ENTRY_HTTP_PROFILE_FORCED_2 = ConfigEntry.from_dict( - {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "no_content_length", "hidden": True} -) - -CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry( - key=CONF_ENABLE_ICY_METADATA, - type=ConfigEntryType.STRING, - options=( - ConfigValueOption("Disabled - do not send ICY metadata", "disabled"), - ConfigValueOption("Profile 1 - basic info", "basic"), - ConfigValueOption("Profile 2 - full info (including image)", "full"), - ), - depends_on=CONF_FLOW_MODE, - default_value="disabled", - label="Try to ingest metadata into stream (ICY)", - category="advanced", - description="Try to ingest metadata into the stream (ICY) to show track info on the player, " - "even when flow mode is enabled.\n\nThis is called ICY metadata and its what is also used by " - "online radio station to inform you what is playing. \n\nBe aware that not all players support " - "this correctly. If you experience issues with playback, try to disable this setting.", -) - - -def create_sample_rates_config_entry( - max_sample_rate: int, - max_bit_depth: int, - safe_max_sample_rate: int = 48000, - safe_max_bit_depth: int = 16, - hidden: bool = False, -) -> ConfigEntry: - """Create sample rates config entry based on player specific helpers.""" - assert CONF_ENTRY_SAMPLE_RATES.options - conf_entry = ConfigEntry.from_dict(CONF_ENTRY_SAMPLE_RATES.to_dict()) - conf_entry.hidden = hidden - options: list[ConfigValueOption] = [] - default_value: list[tuple[int, int]] = [] - for option in CONF_ENTRY_SAMPLE_RATES.options: - if not isinstance(option.value, tuple): - continue - sample_rate, bit_depth = option.value - if sample_rate <= max_sample_rate and bit_depth <= max_bit_depth: - options.append(option) - if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth: - default_value.append(option.value) - conf_entry.options = tuple(options) - conf_entry.default_value = default_value - return conf_entry - - -BASE_PLAYER_CONFIG_ENTRIES = ( - # config entries that are valid for all players - CONF_ENTRY_PLAYER_ICON, - CONF_ENTRY_FLOW_MODE, - CONF_ENTRY_VOLUME_NORMALIZATION, - CONF_ENTRY_AUTO_PLAY, - CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, - CONF_ENTRY_HIDE_PLAYER, - CONF_ENTRY_TTS_PRE_ANNOUNCE, - CONF_ENTRY_SAMPLE_RATES, - CONF_ENTRY_HTTP_PROFILE_FORCED_2, -) diff --git a/music_assistant/common/models/enums.py b/music_assistant/common/models/enums.py deleted file mode 100644 index ecfef45ba..000000000 --- a/music_assistant/common/models/enums.py +++ /dev/null @@ -1,460 +0,0 @@ -"""All enums used by the Music Assistant models.""" - -from __future__ import annotations - -import contextlib -from enum import EnumType, IntEnum, StrEnum - - -class MediaTypeMeta(EnumType): - """Class properties for MediaType.""" - - @property - def ALL(cls) -> list[MediaType]: # noqa: N802 - """All MediaTypes.""" - return [ - MediaType.ARTIST, - MediaType.ALBUM, - MediaType.TRACK, - MediaType.PLAYLIST, - MediaType.RADIO, - ] - - -class MediaType(StrEnum, metaclass=MediaTypeMeta): - """Enum for MediaType.""" - - ARTIST = "artist" - ALBUM = "album" - TRACK = "track" - PLAYLIST = "playlist" - RADIO = "radio" - FOLDER = "folder" - ANNOUNCEMENT = "announcement" - FLOW_STREAM = "flow_stream" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> MediaType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ExternalID(StrEnum): - """Enum with External ID types.""" - - MB_ARTIST = "musicbrainz_artistid" # MusicBrainz Artist ID (or AlbumArtist ID) - MB_ALBUM = "musicbrainz_albumid" # MusicBrainz Album ID - MB_RELEASEGROUP = "musicbrainz_releasegroupid" # MusicBrainz ReleaseGroupID - MB_TRACK = "musicbrainz_trackid" # MusicBrainz Track ID - MB_RECORDING = "musicbrainz_recordingid" # MusicBrainz Recording ID - - ISRC = "isrc" # used to identify unique recordings - BARCODE = "barcode" # EAN-13 barcode for identifying albums - ACOUSTID = "acoustid" # unique fingerprint (id) for a recording - ASIN = "asin" # amazon unique number to identify albums - DISCOGS = "discogs" # id for media item on discogs - TADB = "tadb" # the audio db id - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> ExternalID: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - @property - def is_unique(self) -> bool: - """Return if the ExternalID is unique.""" - return self.is_musicbrainz or self in ( - ExternalID.ACOUSTID, - ExternalID.DISCOGS, - ExternalID.TADB, - ) - - @property - def is_musicbrainz(self) -> bool: - """Return if the ExternalID is a MusicBrainz identifier.""" - return self in ( - ExternalID.MB_RELEASEGROUP, - ExternalID.MB_ALBUM, - ExternalID.MB_TRACK, - ExternalID.MB_ARTIST, - ExternalID.MB_RECORDING, - ) - - -class LinkType(StrEnum): - """Enum with link types.""" - - WEBSITE = "website" - FACEBOOK = "facebook" - TWITTER = "twitter" - LASTFM = "lastfm" - YOUTUBE = "youtube" - INSTAGRAM = "instagram" - SNAPCHAT = "snapchat" - TIKTOK = "tiktok" - DISCOGS = "discogs" - WIKIPEDIA = "wikipedia" - ALLMUSIC = "allmusic" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> LinkType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ImageType(StrEnum): - """Enum with image types.""" - - THUMB = "thumb" - LANDSCAPE = "landscape" - FANART = "fanart" - LOGO = "logo" - CLEARART = "clearart" - BANNER = "banner" - CUTOUT = "cutout" - BACK = "back" - DISCART = "discart" - OTHER = "other" - - @classmethod - def _missing_(cls, value: object) -> ImageType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.OTHER - - -class AlbumType(StrEnum): - """Enum for Album type.""" - - ALBUM = "album" - SINGLE = "single" - COMPILATION = "compilation" - EP = "ep" - PODCAST = "podcast" - AUDIOBOOK = "audiobook" - UNKNOWN = "unknown" - - -class ContentType(StrEnum): - """Enum with audio content/container types supported by ffmpeg.""" - - OGG = "ogg" - FLAC = "flac" - MP3 = "mp3" - AAC = "aac" - MPEG = "mpeg" - ALAC = "alac" - WAV = "wav" - AIFF = "aiff" - WMA = "wma" - M4A = "m4a" - MP4 = "mp4" - M4B = "m4b" - DSF = "dsf" - OPUS = "opus" - WAVPACK = "wv" - PCM_S16LE = "s16le" # PCM signed 16-bit little-endian - PCM_S24LE = "s24le" # PCM signed 24-bit little-endian - PCM_S32LE = "s32le" # PCM signed 32-bit little-endian - PCM_F32LE = "f32le" # PCM 32-bit floating-point little-endian - PCM_F64LE = "f64le" # PCM 64-bit floating-point little-endian - PCM = "pcm" # PCM generic (details determined later) - UNKNOWN = "?" - - @classmethod - def _missing_(cls, value: object) -> ContentType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - @classmethod - def try_parse(cls, string: str) -> ContentType: - """Try to parse ContentType from (url)string/extension.""" - tempstr = string.lower() - if "audio/" in tempstr: - tempstr = tempstr.split("/")[1] - for splitter in (".", ","): - if splitter in tempstr: - for val in tempstr.split(splitter): - with contextlib.suppress(ValueError): - parsed = cls(val.strip()) - if parsed != ContentType.UNKNOWN: - return parsed - tempstr = tempstr.split("?")[0] - tempstr = tempstr.split("&")[0] - tempstr = tempstr.split(";")[0] - tempstr = tempstr.replace("mp4", "m4a") - tempstr = tempstr.replace("mp4a", "m4a") - try: - return cls(tempstr) - except ValueError: - return cls.UNKNOWN - - def is_pcm(self) -> bool: - """Return if contentype is PCM.""" - return self.name.startswith("PCM") - - def is_lossless(self) -> bool: - """Return if format is lossless.""" - return self.is_pcm() or self in ( - ContentType.DSF, - ContentType.FLAC, - ContentType.AIFF, - ContentType.WAV, - ContentType.ALAC, - ContentType.WAVPACK, - ) - - @classmethod - def from_bit_depth(cls, bit_depth: int, floating_point: bool = False) -> ContentType: - """Return (PCM) Contenttype from PCM bit depth.""" - if floating_point and bit_depth > 32: - return cls.PCM_F64LE - if floating_point: - return cls.PCM_F32LE - if bit_depth == 16: - return cls.PCM_S16LE - if bit_depth == 24: - return cls.PCM_S24LE - return cls.PCM_S32LE - - -class QueueOption(StrEnum): - """Enum representation of the queue (play) options. - - - PLAY -> Insert new item(s) in queue at the current position and start playing. - - REPLACE -> Replace entire queue contents with the new items and start playing from index 0. - - NEXT -> Insert item(s) after current playing/buffered item. - - REPLACE_NEXT -> Replace item(s) after current playing/buffered item. - - ADD -> Add new item(s) to the queue (at the end if shuffle is not enabled). - """ - - PLAY = "play" - REPLACE = "replace" - NEXT = "next" - REPLACE_NEXT = "replace_next" - ADD = "add" - - -class RepeatMode(StrEnum): - """Enum with repeat modes.""" - - OFF = "off" # no repeat at all - ONE = "one" # repeat one/single track - ALL = "all" # repeat entire queue - - -class PlayerState(StrEnum): - """Enum for the (playback)state of a player.""" - - IDLE = "idle" - PAUSED = "paused" - PLAYING = "playing" - - -class PlayerType(StrEnum): - """Enum with possible Player Types. - - player: A regular player. - stereo_pair: Same as player but a dedicated stereo pair of 2 speakers. - group: A (dedicated) (sync)group player or (universal) playergroup. - """ - - PLAYER = "player" - STEREO_PAIR = "stereo_pair" - GROUP = "group" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> PlayerType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class PlayerFeature(StrEnum): - """Enum with possible Player features. - - power: The player has a dedicated power control. - volume: The player supports adjusting the volume. - mute: The player supports muting the volume. - sync: The player supports syncing with other players (of the same platform). - accurate_time: The player provides millisecond accurate timing information. - seek: The player supports seeking to a specific. - enqueue: The player supports (en)queuing of media items natively. - """ - - POWER = "power" - VOLUME_SET = "volume_set" - VOLUME_MUTE = "volume_mute" - PAUSE = "pause" - SYNC = "sync" - SEEK = "seek" - NEXT_PREVIOUS = "next_previous" - PLAY_ANNOUNCEMENT = "play_announcement" - ENQUEUE = "enqueue" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> PlayerFeature: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class EventType(StrEnum): - """Enum with possible Events.""" - - PLAYER_ADDED = "player_added" - PLAYER_UPDATED = "player_updated" - PLAYER_REMOVED = "player_removed" - PLAYER_SETTINGS_UPDATED = "player_settings_updated" - QUEUE_ADDED = "queue_added" - QUEUE_UPDATED = "queue_updated" - QUEUE_ITEMS_UPDATED = "queue_items_updated" - QUEUE_TIME_UPDATED = "queue_time_updated" - MEDIA_ITEM_PLAYED = "media_item_played" - SHUTDOWN = "application_shutdown" - MEDIA_ITEM_ADDED = "media_item_added" - MEDIA_ITEM_UPDATED = "media_item_updated" - MEDIA_ITEM_DELETED = "media_item_deleted" - PROVIDERS_UPDATED = "providers_updated" - PLAYER_CONFIG_UPDATED = "player_config_updated" - SYNC_TASKS_UPDATED = "sync_tasks_updated" - AUTH_SESSION = "auth_session" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> EventType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ProviderFeature(StrEnum): - """Enum with features for a Provider.""" - - # - # MUSICPROVIDER FEATURES - # - - # browse/explore/recommendations - BROWSE = "browse" - SEARCH = "search" - RECOMMENDATIONS = "recommendations" - - # library feature per mediatype - LIBRARY_ARTISTS = "library_artists" - LIBRARY_ALBUMS = "library_albums" - LIBRARY_TRACKS = "library_tracks" - LIBRARY_PLAYLISTS = "library_playlists" - LIBRARY_RADIOS = "library_radios" - - # additional library features - ARTIST_ALBUMS = "artist_albums" - ARTIST_TOPTRACKS = "artist_toptracks" - - # library edit (=add/remove) feature per mediatype - LIBRARY_ARTISTS_EDIT = "library_artists_edit" - LIBRARY_ALBUMS_EDIT = "library_albums_edit" - LIBRARY_TRACKS_EDIT = "library_tracks_edit" - LIBRARY_PLAYLISTS_EDIT = "library_playlists_edit" - LIBRARY_RADIOS_EDIT = "library_radios_edit" - - # if we can grab 'similar tracks' from the music provider - # used to generate dynamic playlists - SIMILAR_TRACKS = "similar_tracks" - - # playlist-specific features - PLAYLIST_TRACKS_EDIT = "playlist_tracks_edit" - PLAYLIST_CREATE = "playlist_create" - - # - # PLAYERPROVIDER FEATURES - # - SYNC_PLAYERS = "sync_players" - REMOVE_PLAYER = "remove_player" - - # - # METADATAPROVIDER FEATURES - # - ARTIST_METADATA = "artist_metadata" - ALBUM_METADATA = "album_metadata" - TRACK_METADATA = "track_metadata" - - # - # PLUGIN FEATURES - # - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> ProviderFeature: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class ProviderType(StrEnum): - """Enum with supported provider types.""" - - MUSIC = "music" - PLAYER = "player" - METADATA = "metadata" - PLUGIN = "plugin" - CORE = "core" - - -class ConfigEntryType(StrEnum): - """Enum for the type of a config entry.""" - - BOOLEAN = "boolean" - STRING = "string" - SECURE_STRING = "secure_string" - INTEGER = "integer" - FLOAT = "float" - LABEL = "label" - INTEGER_TUPLE = "integer_tuple" - DIVIDER = "divider" - ACTION = "action" - ICON = "icon" - ALERT = "alert" - UNKNOWN = "unknown" - - @classmethod - def _missing_(cls, value: object) -> ConfigEntryType: # noqa: ARG003 - """Set default enum member if an unknown value is provided.""" - return cls.UNKNOWN - - -class StreamType(StrEnum): - """Enum for the type of streamdetails.""" - - HTTP = "http" # regular http stream - ENCRYPTED_HTTP = "encrypted_http" # encrypted http stream - HLS = "hls" # http HLS stream - ICY = "icy" # http stream with icy metadata - LOCAL_FILE = "local_file" - CUSTOM = "custom" - - -class CacheCategory(IntEnum): - """Enum with predefined cache categories.""" - - DEFAULT = 0 - MUSIC_SEARCH = 1 - MUSIC_ALBUM_TRACKS = 2 - MUSIC_ARTIST_TRACKS = 3 - MUSIC_ARTIST_ALBUMS = 4 - MUSIC_PLAYLIST_TRACKS = 5 - MUSIC_PROVIDER_ITEM = 6 - PLAYER_QUEUE_STATE = 7 - MEDIA_INFO = 8 - LIBRARY_ITEMS = 9 - - -class VolumeNormalizationMode(StrEnum): - """Enum with possible VolumeNormalization modes.""" - - DISABLED = "disabled" - DYNAMIC = "dynamic" - MEASUREMENT_ONLY = "measurement_only" - FALLBACK_FIXED_GAIN = "fallback_fixed_gain" - FIXED_GAIN = "fixed_gain" - FALLBACK_DYNAMIC = "fallback_dynamic" diff --git a/music_assistant/common/models/errors.py b/music_assistant/common/models/errors.py deleted file mode 100644 index 5e04af314..000000000 --- a/music_assistant/common/models/errors.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Custom errors and exceptions.""" - - -class MusicAssistantError(Exception): - """Custom Exception for all errors.""" - - error_code = 0 - - def __init_subclass__(cls, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - """Register a subclass.""" - super().__init_subclass__(*args, **kwargs) - ERROR_MAP[cls.error_code] = cls - - -# mapping from error_code to Exception class -ERROR_MAP: dict[int, type] = {0: MusicAssistantError, 999: MusicAssistantError} - - -class ProviderUnavailableError(MusicAssistantError): - """Error raised when trying to access mediaitem of unavailable provider.""" - - error_code = 1 - - -class MediaNotFoundError(MusicAssistantError): - """Error raised when trying to access non existing media item.""" - - error_code = 2 - - -class InvalidDataError(MusicAssistantError): - """Error raised when an object has invalid data.""" - - error_code = 3 - - -class AlreadyRegisteredError(MusicAssistantError): - """Error raised when a duplicate music provider or player is registered.""" - - error_code = 4 - - -class SetupFailedError(MusicAssistantError): - """Error raised when setup of a provider or player failed.""" - - error_code = 5 - - -class LoginFailed(MusicAssistantError): - """Error raised when a login failed.""" - - error_code = 6 - - -class AudioError(MusicAssistantError): - """Error raised when an issue arrised when processing audio.""" - - error_code = 7 - - -class QueueEmpty(MusicAssistantError): - """Error raised when trying to start queue stream while queue is empty.""" - - error_code = 8 - - -class UnsupportedFeaturedException(MusicAssistantError): - """Error raised when a feature is not supported.""" - - error_code = 9 - - -class PlayerUnavailableError(MusicAssistantError): - """Error raised when trying to access non-existing or unavailable player.""" - - error_code = 10 - - -class PlayerCommandFailed(MusicAssistantError): - """Error raised when a command to a player failed execution.""" - - error_code = 11 - - -class InvalidCommand(MusicAssistantError): - """Error raised when an unknown command is requested on the API.""" - - error_code = 12 - - -class UnplayableMediaError(MusicAssistantError): - """Error thrown when a MediaItem cannot be played properly.""" - - error_code = 13 - - -class InvalidProviderURI(MusicAssistantError): - """Error thrown when a provider URI does not match a known format.""" - - error_code = 14 - - -class InvalidProviderID(MusicAssistantError): - """Error thrown when a provider media item identifier does not match a known format.""" - - error_code = 15 - - -class RetriesExhausted(MusicAssistantError): - """Error thrown when a retries to a given provider URI have been exhausted.""" - - error_code = 16 - - -class ResourceTemporarilyUnavailable(MusicAssistantError): - """Error thrown when a resource is temporarily unavailable.""" - - def __init__(self, *args: object, backoff_time: int = 0) -> None: - """Initialize.""" - super().__init__(*args) - self.backoff_time = backoff_time - - error_code = 17 - - -class ProviderPermissionDenied(MusicAssistantError): - """Error thrown when a provider action is denied because of permissions.""" - - error_code = 18 - - -class ActionUnavailable(MusicAssistantError): - """Error thrown when a action is denied because is is (temporary) unavailable/not possible.""" - - error_code = 19 diff --git a/music_assistant/common/models/event.py b/music_assistant/common/models/event.py deleted file mode 100644 index a997808f1..000000000 --- a/music_assistant/common/models/event.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Model for Music Assistant Event.""" - -from dataclasses import dataclass, field -from typing import Any - -from mashumaro.mixins.orjson import DataClassORJSONMixin - -from music_assistant.common.helpers.json import get_serializable_value -from music_assistant.common.models.enums import EventType - - -@dataclass -class MassEvent(DataClassORJSONMixin): - """Representation of an Event emitted in/by Music Assistant.""" - - event: EventType - object_id: str | None = None # player_id, queue_id or uri - data: Any = field( - default=None, - metadata={"serialize": lambda v: get_serializable_value(v)}, - ) diff --git a/music_assistant/common/models/media_items.py b/music_assistant/common/models/media_items.py deleted file mode 100644 index 024f8d072..000000000 --- a/music_assistant/common/models/media_items.py +++ /dev/null @@ -1,585 +0,0 @@ -"""Models and helpers for media items.""" - -from __future__ import annotations - -from collections.abc import Iterable, Sequence -from dataclasses import dataclass, field, fields -from typing import TYPE_CHECKING, Any, TypeGuard, TypeVar, cast - -from mashumaro import DataClassDictMixin - -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.helpers.uri import create_uri -from music_assistant.common.helpers.util import create_sort_name, is_valid_uuid, merge_lists -from music_assistant.common.models.enums import ( - AlbumType, - ContentType, - ExternalID, - ImageType, - LinkType, - MediaType, -) -from music_assistant.common.models.errors import InvalidDataError - -MetadataTypes = int | bool | str | list[str] - -_T = TypeVar("_T") - - -class UniqueList(list[_T]): - """Custom list that ensures the inserted items are unique.""" - - def __init__(self, iterable: Iterable[_T] | None = None) -> None: - """Initialize.""" - if not iterable: - super().__init__() - return - seen: set[_T] = set() - seen_add = seen.add - super().__init__(x for x in iterable if not (x in seen or seen_add(x))) - - def append(self, item: _T) -> None: - """Append item.""" - if item in self: - return - super().append(item) - - def extend(self, other: Iterable[_T]) -> None: - """Extend list.""" - other = [x for x in other if x not in self] - super().extend(other) - - -@dataclass(kw_only=True) -class AudioFormat(DataClassDictMixin): - """Model for AudioFormat details.""" - - content_type: ContentType = ContentType.UNKNOWN - sample_rate: int = 44100 - bit_depth: int = 16 - channels: int = 2 - output_format_str: str = "" - bit_rate: int | None = None # optional bitrate in kbps - - def __post_init__(self) -> None: - """Execute actions after init.""" - if not self.output_format_str and self.content_type.is_pcm(): - self.output_format_str = ( - f"pcm;codec=pcm;rate={self.sample_rate};" - f"bitrate={self.bit_depth};channels={self.channels}" - ) - elif not self.output_format_str: - self.output_format_str = self.content_type.value - if self.bit_rate and self.bit_rate > 100000: - # correct bit rate in bits per second to kbps - self.bit_rate = int(self.bit_rate / 1000) - - @property - def quality(self) -> int: - """Calculate quality score.""" - if self.content_type.is_lossless(): - # lossless content is scored very high based on sample rate and bit depth - return int(self.sample_rate / 1000) + self.bit_depth - # lossy content, bit_rate is most important score - # but prefer some codecs over others - # calculate a rough score based on bit rate per channel - bit_rate = self.bit_rate or 320 - bit_rate_score = (bit_rate / self.channels) / 100 - if self.content_type in (ContentType.AAC, ContentType.OGG): - bit_rate_score += 1 - return int(bit_rate_score) - - @property - def pcm_sample_size(self) -> int: - """Return the PCM sample size.""" - return int(self.sample_rate * (self.bit_depth / 8) * self.channels) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, AudioFormat): - return False - return self.output_format_str == other.output_format_str - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - # bit_rate is now optional. Set default value to keep compatibility - # TODO: remove this after release of MA 2.5 - d["bit_rate"] = d["bit_rate"] or 0 - return d - - -@dataclass(kw_only=True) -class ProviderMapping(DataClassDictMixin): - """Model for a MediaItem's provider mapping details.""" - - item_id: str - provider_domain: str - provider_instance: str - available: bool = True - # quality/audio details (streamable content only) - audio_format: AudioFormat = field(default_factory=AudioFormat) - # url = link to provider details page if exists - url: str | None = None - # optional details to store provider specific details - details: str | None = None - - @property - def quality(self) -> int: - """Return quality score.""" - quality = self.audio_format.quality - # append provider score so filebased providers are scored higher - return quality + self.priority - - @property - def priority(self) -> int: - """Return priority score to sort local providers before online.""" - if not (local_provs := get_global_cache_value("non_streaming_providers")): - # this is probably the client - return 0 - if TYPE_CHECKING: - local_provs = cast(set[str], local_provs) - if self.provider_domain in ("filesystem_local", "filesystem_smb"): - return 2 - if self.provider_instance in local_provs: - return 1 - return 0 - - def __hash__(self) -> int: - """Return custom hash.""" - return hash((self.provider_instance, self.item_id)) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, ProviderMapping): - return False - return self.provider_instance == other.provider_instance and self.item_id == other.item_id - - -@dataclass(frozen=True, kw_only=True) -class MediaItemLink(DataClassDictMixin): - """Model for a link.""" - - type: LinkType - url: str - - def __hash__(self) -> int: - """Return custom hash.""" - return hash(self.type) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItemLink): - return False - return self.url == other.url - - -@dataclass(frozen=True, kw_only=True) -class MediaItemImage(DataClassDictMixin): - """Model for a image.""" - - type: ImageType - path: str - provider: str # provider lookup key (only use instance id for fileproviders) - remotely_accessible: bool = False # url that is accessible from anywhere - - def __hash__(self) -> int: - """Return custom hash.""" - return hash((self.type.value, self.provider, self.path)) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItemImage): - return False - return self.__hash__() == other.__hash__() - - -@dataclass(frozen=True, kw_only=True) -class MediaItemChapter(DataClassDictMixin): - """Model for a chapter.""" - - chapter_id: int - position_start: float - position_end: float | None = None - title: str | None = None - - def __hash__(self) -> int: - """Return custom hash.""" - return hash(self.chapter_id) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItemChapter): - return False - return self.chapter_id == other.chapter_id - - -@dataclass(kw_only=True) -class MediaItemMetadata(DataClassDictMixin): - """Model for a MediaItem's metadata.""" - - description: str | None = None - review: str | None = None - explicit: bool | None = None - # NOTE: images is a list of available images, sorted by preference - images: UniqueList[MediaItemImage] | None = None - genres: set[str] | None = None - mood: str | None = None - style: str | None = None - copyright: str | None = None - lyrics: str | None = None # tracks only - label: str | None = None - links: set[MediaItemLink] | None = None - chapters: UniqueList[MediaItemChapter] | None = None - performers: set[str] | None = None - preview: str | None = None - popularity: int | None = None - # last_refresh: timestamp the (full) metadata was last collected - last_refresh: int | None = None - - def update( - self, - new_values: MediaItemMetadata, - ) -> MediaItemMetadata: - """Update metadata (in-place) with new values.""" - if not new_values: - return self - for fld in fields(self): - new_val = getattr(new_values, fld.name) - if new_val is None: - continue - cur_val = getattr(self, fld.name) - if isinstance(cur_val, list) and isinstance(new_val, list): - new_val = UniqueList(merge_lists(cur_val, new_val)) - setattr(self, fld.name, new_val) - elif isinstance(cur_val, set) and isinstance(new_val, set | list | tuple): - cur_val.update(new_val) - elif new_val and fld.name in ( - "popularity", - "last_refresh", - "cache_checksum", - ): - # some fields are always allowed to be overwritten - # (such as checksum and last_refresh) - setattr(self, fld.name, new_val) - elif cur_val is None: - setattr(self, fld.name, new_val) - return self - - -@dataclass(kw_only=True) -class _MediaItemBase(DataClassDictMixin): - """Base representation of a Media Item or ItemMapping item object.""" - - item_id: str - provider: str # provider instance id or provider domain - name: str - version: str = "" - # sort_name will be auto generated if omitted - sort_name: str | None = None - # uri is auto generated, do not override unless really needed - uri: str | None = None - external_ids: set[tuple[ExternalID, str]] = field(default_factory=set) - media_type: MediaType = MediaType.UNKNOWN - - def __post_init__(self) -> None: - """Call after init.""" - if self.uri is None: - self.uri = create_uri(self.media_type, self.provider, self.item_id) - if self.sort_name is None: - self.sort_name = create_sort_name(self.name) - - def get_external_id(self, external_id_type: ExternalID) -> str | None: - """Get (the first instance) of given External ID or None if not found.""" - for ext_id in self.external_ids: - if ext_id[0] != external_id_type: - continue - return ext_id[1] - return None - - def add_external_id(self, external_id_type: ExternalID, value: str) -> None: - """Add ExternalID.""" - if external_id_type.is_musicbrainz and not is_valid_uuid(value): - msg = f"Invalid MusicBrainz identifier: {value}" - raise InvalidDataError(msg) - if external_id_type.is_unique and ( - existing := next((x for x in self.external_ids if x[0] == external_id_type), None) - ): - self.external_ids.remove(existing) - self.external_ids.add((external_id_type, value)) - - @property - def mbid(self) -> str | None: - """Return MusicBrainz ID.""" - if self.media_type == MediaType.ARTIST: - return self.get_external_id(ExternalID.MB_ARTIST) - if self.media_type == MediaType.ALBUM: - return self.get_external_id(ExternalID.MB_ALBUM) - if self.media_type == MediaType.TRACK: - return self.get_external_id(ExternalID.MB_RECORDING) - return None - - @mbid.setter - def mbid(self, value: str) -> None: - """Set MusicBrainz External ID.""" - if self.media_type == MediaType.ARTIST: - self.add_external_id(ExternalID.MB_ARTIST, value) - elif self.media_type == MediaType.ALBUM: - self.add_external_id(ExternalID.MB_ALBUM, value) - elif self.media_type == MediaType.TRACK: - # NOTE: for tracks we use the recording id to - # differentiate a unique recording - # and not the track id (as that is just the reference - # of the recording on a specific album) - self.add_external_id(ExternalID.MB_RECORDING, value) - return - - def __hash__(self) -> int: - """Return custom hash.""" - return hash(self.uri) - - def __eq__(self, other: object) -> bool: - """Check equality of two items.""" - if not isinstance(other, MediaItem | ItemMapping): - return False - return self.uri == other.uri - - -@dataclass(kw_only=True) -class MediaItem(_MediaItemBase): - """Base representation of a media item.""" - - __eq__ = _MediaItemBase.__eq__ - - provider_mappings: set[ProviderMapping] - # optional fields below - metadata: MediaItemMetadata = field(default_factory=MediaItemMetadata) - favorite: bool = False - position: int | None = None # required for playlist tracks, optional for all other - - def __hash__(self) -> int: - """Return hash of MediaItem.""" - return super().__hash__() - - @property - def available(self) -> bool: - """Return (calculated) availability.""" - if not (available_providers := get_global_cache_value("unique_providers")): - # this is probably the client - return any(x.available for x in self.provider_mappings) - if TYPE_CHECKING: - available_providers = cast(set[str], available_providers) - for x in self.provider_mappings: - if available_providers.intersection({x.provider_domain, x.provider_instance}): - return True - return False - - @property - def image(self) -> MediaItemImage | None: - """Return (first/random) image/thumb from metadata (if any).""" - if self.metadata is None or self.metadata.images is None: - return None - return next((x for x in self.metadata.images if x.type == ImageType.THUMB), None) - - -@dataclass(kw_only=True) -class ItemMapping(_MediaItemBase): - """Representation of a minimized item object.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - available: bool = True - image: MediaItemImage | None = None - - @classmethod - def from_item(cls, item: MediaItem | ItemMapping) -> ItemMapping: - """Create ItemMapping object from regular item.""" - if isinstance(item, ItemMapping): - return item - thumb_image = None - if item.metadata and item.metadata.images: - for img in item.metadata.images: - if img.type != ImageType.THUMB: - continue - thumb_image = img - break - return cls.from_dict( - {**item.to_dict(), "image": thumb_image.to_dict() if thumb_image else None} - ) - - -@dataclass(kw_only=True) -class Artist(MediaItem): - """Model for an artist.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.ARTIST - - -@dataclass(kw_only=True) -class Album(MediaItem): - """Model for an album.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.ALBUM - version: str = "" - year: int | None = None - artists: UniqueList[Artist | ItemMapping] = field(default_factory=UniqueList) - album_type: AlbumType = AlbumType.UNKNOWN - - @property - def artist_str(self) -> str: - """Return (combined) artist string for track.""" - return "/".join(x.name for x in self.artists) - - -@dataclass(kw_only=True) -class Track(MediaItem): - """Model for a track.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.TRACK - duration: int = 0 - version: str = "" - artists: UniqueList[Artist | ItemMapping] = field(default_factory=UniqueList) - album: Album | ItemMapping | None = None # required for album tracks - disc_number: int = 0 # required for album tracks - track_number: int = 0 # required for album tracks - - @property - def has_chapters(self) -> bool: - """ - Return boolean if this Track has chapters. - - This is often an indicator that this track is an episode from a - Podcast or AudioBook. - """ - if not self.metadata: - return False - if not self.metadata.chapters: - return False - return len(self.metadata.chapters) > 1 - - @property - def image(self) -> MediaItemImage | None: - """Return (first) image from metadata (prefer album).""" - if isinstance(self.album, Album) and self.album.image: - return self.album.image - return super().image - - @property - def artist_str(self) -> str: - """Return (combined) artist string for track.""" - return "/".join(x.name for x in self.artists) - - -@dataclass(kw_only=True) -class PlaylistTrack(Track): - """ - Model for a track on a playlist. - - Same as regular Track but with explicit and required definition of position. - """ - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - position: int - - -@dataclass(kw_only=True) -class Playlist(MediaItem): - """Model for a playlist.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.PLAYLIST - owner: str = "" - is_editable: bool = False - - # cache_checksum: optional value to (in)validate cache - # detect changes to the playlist tracks listing - cache_checksum: str | None = None - - -@dataclass(kw_only=True) -class Radio(MediaItem): - """Model for a radio station.""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.RADIO - duration: int = 172800 - - -@dataclass(kw_only=True) -class BrowseFolder(MediaItem): - """Representation of a Folder used in Browse (which contains media items).""" - - __hash__ = _MediaItemBase.__hash__ - __eq__ = _MediaItemBase.__eq__ - - media_type: MediaType = MediaType.FOLDER - # path: the path (in uri style) to/for this browse folder - path: str = "" - # label: a labelid that needs to be translated by the frontend - label: str = "" - provider_mappings: set[ProviderMapping] = field(default_factory=set) - - def __post_init__(self) -> None: - """Call after init.""" - super().__post_init__() - if not self.path: - self.path = f"{self.provider}://{self.item_id}" - if not self.provider_mappings: - self.provider_mappings.add( - ProviderMapping( - item_id=self.item_id, - provider_domain=self.provider, - provider_instance=self.provider, - ) - ) - - -MediaItemType = Artist | Album | PlaylistTrack | Track | Radio | Playlist | BrowseFolder - - -@dataclass(kw_only=True) -class SearchResults(DataClassDictMixin): - """Model for results from a search query.""" - - artists: Sequence[Artist | ItemMapping] = field(default_factory=list) - albums: Sequence[Album | ItemMapping] = field(default_factory=list) - tracks: Sequence[Track | ItemMapping] = field(default_factory=list) - playlists: Sequence[Playlist | ItemMapping] = field(default_factory=list) - radio: Sequence[Radio | ItemMapping] = field(default_factory=list) - - -def media_from_dict(media_item: dict[str, Any]) -> MediaItemType | ItemMapping: - """Return MediaItem from dict.""" - if "provider_mappings" not in media_item: - return ItemMapping.from_dict(media_item) - if media_item["media_type"] == "artist": - return Artist.from_dict(media_item) - if media_item["media_type"] == "album": - return Album.from_dict(media_item) - if media_item["media_type"] == "track": - return Track.from_dict(media_item) - if media_item["media_type"] == "playlist": - return Playlist.from_dict(media_item) - if media_item["media_type"] == "radio": - return Radio.from_dict(media_item) - raise InvalidDataError("Unknown media type") - - -def is_track(val: MediaItem) -> TypeGuard[Track]: - """Return true if this MediaItem is a track.""" - return val.media_type == MediaType.TRACK diff --git a/music_assistant/common/models/player.py b/music_assistant/common/models/player.py deleted file mode 100644 index 35fb66ad0..000000000 --- a/music_assistant/common/models/player.py +++ /dev/null @@ -1,171 +0,0 @@ -"""Model(s) for Player.""" - -from __future__ import annotations - -import time -from dataclasses import dataclass, field -from typing import Any - -from mashumaro import DataClassDictMixin - -from .enums import MediaType, PlayerFeature, PlayerState, PlayerType - - -@dataclass(frozen=True) -class DeviceInfo(DataClassDictMixin): - """Model for a player's deviceinfo.""" - - model: str = "Unknown model" - address: str = "" - manufacturer: str = "Unknown Manufacturer" - - -@dataclass -class PlayerMedia(DataClassDictMixin): - """Metadata of Media loading/loaded into a player.""" - - uri: str # uri or other identifier of the loaded media - media_type: MediaType = MediaType.UNKNOWN - title: str | None = None # optional - artist: str | None = None # optional - album: str | None = None # optional - image_url: str | None = None # optional - duration: int | None = None # optional - queue_id: str | None = None # only present for requests from queue controller - queue_item_id: str | None = None # only present for requests from queue controller - custom_data: dict[str, Any] | None = None # optional - - -@dataclass -class Player(DataClassDictMixin): - """Representation of a Player within Music Assistant.""" - - player_id: str - provider: str # instance_id of the player provider - type: PlayerType - name: str - available: bool - powered: bool - device_info: DeviceInfo - supported_features: tuple[PlayerFeature, ...] = field(default=()) - - elapsed_time: float | None = None - elapsed_time_last_updated: float | None = None - state: PlayerState | None = None - volume_level: int | None = None - volume_muted: bool | None = None - - # group_childs: Return list of player group child id's or synced child`s. - # - If this player is a dedicated group player, - # returns all child id's of the players in the group. - # - If this is a syncgroup of players from the same platform (e.g. sonos), - # this will return the id's of players synced to this player, - # and this may include the player's own id. - group_childs: set[str] = field(default_factory=set) - - # active_source: return active source for this player - # can be set to a MA queue id or some player specific source - active_source: str | None = None - - # active_source: return player_id of the active group for this player (if any) - # if the player is grouped and a group is active, - # this should be set to the group's player_id by the group player implementation. - active_group: str | None = None - - # current_media: return current active/loaded item on the player - # this may be a MA queue item, url, uri or some provider specific string - # includes metadata if supported by the provider/player - current_media: PlayerMedia | None = None - - # synced_to: player_id of the player this player is currently synced to - # also referred to as "sync master" - synced_to: str | None = None - - # enabled_by_default: if the player is enabled by default - # can be used by a player provider to exclude some sort of players - enabled_by_default: bool = True - - # needs_poll: bool that can be set by the player(provider) - # if this player needs to be polled for state changes by the player manager - needs_poll: bool = False - - # poll_interval: a (dynamic) interval in seconds to poll the player (used with needs_poll) - poll_interval: int = 30 - - # - # THE BELOW ATTRIBUTES ARE MANAGED BY CONFIG AND THE PLAYER MANAGER - # - - # enabled: if the player is enabled - # will be set by the player manager based on config - # a disabled player is hidden in the UI and updates will not be processed - # nor will it be added to the HA integration - enabled: bool = True - - # hidden: if the player is hidden in the UI - # will be set by the player manager based on config - # a hidden player is hidden in the UI only but can still be controlled - hidden: bool = False - - # icon: material design icon for this player - # will be set by the player manager based on config - icon: str = "mdi-speaker" - - # group_volume: if the player is a player group or syncgroup master, - # this will return the average volume of all child players - # if not a group player, this is just the player's volume - group_volume: int = 100 - - # display_name: return final/corrected name of the player - # always prefers any overridden name from settings - display_name: str = "" - - # extra_data: any additional data to store on the player object - # and pass along freely - extra_data: dict[str, Any] = field(default_factory=dict) - - # announcement_in_progress boolean to indicate there's an announcement in progress. - announcement_in_progress: bool = False - - # last_poll: when was the player last polled (used with needs_poll) - last_poll: float = 0 - - # internal use only - _prev_volume_level: int = 0 - - @property - def corrected_elapsed_time(self) -> float | None: - """Return the corrected/realtime elapsed time.""" - if self.elapsed_time is None or self.elapsed_time_last_updated is None: - return None - if self.state == PlayerState.PLAYING: - return self.elapsed_time + (time.time() - self.elapsed_time_last_updated) - return self.elapsed_time - - @property - def current_item_id(self) -> str | None: - """Return current_item_id from current_media (if exists).""" - if self.current_media: - return self.current_media.uri - return None - - @current_item_id.setter - def current_item_id(self, uri: str) -> None: - """Set current_item_id (for backwards compatibility).""" - self.current_media = PlayerMedia(uri) - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - # TEMP: convert values to prevent api breakage - # this may be removed after 2.3 has been launched to stable - if self.elapsed_time is None: - d["elapsed_time"] = 0 - if self.elapsed_time_last_updated is None: - d["elapsed_time_last_updated"] = 0 - if self.volume_level is None: - d["volume_level"] = 0 - if self.volume_muted is None: - d["volume_muted"] = False - if self.state is None: - d["state"] = PlayerState.IDLE - return d diff --git a/music_assistant/common/models/player_queue.py b/music_assistant/common/models/player_queue.py deleted file mode 100644 index dbeca3257..000000000 --- a/music_assistant/common/models/player_queue.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Model(s) for PlayerQueue.""" - -from __future__ import annotations - -import time -from dataclasses import dataclass, field -from typing import Any, Self - -from mashumaro import DataClassDictMixin - -from music_assistant.common.models.media_items import MediaItemType -from music_assistant.constants import FALLBACK_DURATION - -from .enums import PlayerState, RepeatMode -from .queue_item import QueueItem - - -@dataclass -class PlayLogEntry: - """Representation of a PlayLogEntry within Music Assistant.""" - - queue_item_id: str - duration: int = FALLBACK_DURATION - seconds_streamed: float | None = None - - -@dataclass -class PlayerQueue(DataClassDictMixin): - """Representation of a PlayerQueue within Music Assistant.""" - - queue_id: str - active: bool - display_name: str - available: bool - items: int - - shuffle_enabled: bool = False - repeat_mode: RepeatMode = RepeatMode.OFF - dont_stop_the_music_enabled: bool = False - # current_index: index that is active (e.g. being played) by the player - current_index: int | None = None - # index_in_buffer: index that has been preloaded/buffered by the player - index_in_buffer: int | None = None - elapsed_time: float = 0 - elapsed_time_last_updated: float = time.time() - state: PlayerState = PlayerState.IDLE - current_item: QueueItem | None = None - next_item: QueueItem | None = None - radio_source: list[MediaItemType] = field(default_factory=list) - enqueued_media_items: list[MediaItemType] = field(default_factory=list) - flow_mode: bool = False - resume_pos: int = 0 - flow_mode_stream_log: list[PlayLogEntry] = field(default_factory=list) - next_track_enqueued: str | None = None - - @property - def corrected_elapsed_time(self) -> float: - """Return the corrected/realtime elapsed time.""" - return self.elapsed_time + (time.time() - self.elapsed_time_last_updated) - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - d.pop("flow_mode_stream_log", None) - d.pop("enqueued_media_items", None) - d.pop("next_track_enqueued", None) - return d - - def to_cache(self) -> dict[str, Any]: - """Return the dict that is suitable for storing into the cache db.""" - d = self.to_dict() - d.pop("current_item", None) - d.pop("next_item", None) - d.pop("index_in_buffer", None) - d.pop("flow_mode", None) - return d - - @classmethod - def from_cache(cls, d: dict[Any, Any]) -> Self: - """Restore a PlayerQueue from a cache dict.""" - d.pop("current_item", None) - d.pop("next_item", None) - d.pop("index_in_buffer", None) - d.pop("flow_mode", None) - d.pop("enqueued_media_items", None) - d.pop("next_track_enqueued", None) - d.pop("flow_mode_stream_log", None) - return cls.from_dict(d) diff --git a/music_assistant/common/models/provider.py b/music_assistant/common/models/provider.py deleted file mode 100644 index 7a7d02191..000000000 --- a/music_assistant/common/models/provider.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Models for providers and plugins in the MA ecosystem.""" - -from __future__ import annotations - -import asyncio -from dataclasses import dataclass, field -from typing import Any - -from mashumaro.mixins.orjson import DataClassORJSONMixin - -from music_assistant.common.helpers.json import load_json_file - -from .enums import MediaType, ProviderFeature, ProviderType - - -@dataclass -class ProviderManifest(DataClassORJSONMixin): - """ProviderManifest, details of a provider.""" - - type: ProviderType - domain: str - name: str - description: str - codeowners: list[str] - - # optional params - - # requirements: list of (pip style) python packages required for this provider - requirements: list[str] = field(default_factory=list) - # documentation: link/url to documentation. - documentation: str | None = None - # multi_instance: whether multiple instances of the same provider are allowed/possible - multi_instance: bool = False - # builtin: whether this provider is a system/builtin provider, loaded by default - builtin: bool = False - # allow_disable: whether this provider can be disabled (used with builtin) - allow_disable: bool = True - # depends_on: depends on another provider to function - depends_on: str | None = None - # icon: name of the material design icon (https://pictogrammers.com/library/mdi) - icon: str | None = None - # icon_svg: svg icon (full xml string) - # if this attribute is omitted and an icon.svg is found in the provider - # folder, the file contents will be read instead. - icon_svg: str | None = None - # icon_svg_dark: optional separate dark svg icon (full xml string) - # if this attribute is omitted and an icon_dark.svg is found in the provider - # folder, the file contents will be read instead. - icon_svg_dark: str | None = None - # mdns_discovery: list of mdns types to discover - mdns_discovery: list[str] | None = None - - @classmethod - async def parse(cls, manifest_file: str) -> ProviderManifest: - """Parse ProviderManifest from file.""" - return await load_json_file(manifest_file, ProviderManifest) - - -@dataclass -class ProviderInstance(DataClassORJSONMixin): - """Provider instance details when a provider is serialized over the api.""" - - type: ProviderType - domain: str - name: str - instance_id: str - lookup_key: str - supported_features: list[ProviderFeature] - available: bool - icon: str | None = None - is_streaming_provider: bool | None = None # music providers only - - -@dataclass -class SyncTask: - """Description of a Sync task/job of a musicprovider.""" - - provider_domain: str - provider_instance: str - media_types: tuple[MediaType, ...] - task: asyncio.Task[None] | None - - def to_dict(self) -> dict[str, Any]: - """Return SyncTask as (serializable) dict.""" - # ruff: noqa:ARG002 - return { - "provider_domain": self.provider_domain, - "provider_instance": self.provider_instance, - "media_types": [x.value for x in self.media_types], - } diff --git a/music_assistant/common/models/queue_item.py b/music_assistant/common/models/queue_item.py deleted file mode 100644 index 8290b3871..000000000 --- a/music_assistant/common/models/queue_item.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Model a QueueItem.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Self -from uuid import uuid4 - -from mashumaro import DataClassDictMixin - -from .enums import MediaType -from .media_items import ItemMapping, MediaItemImage, Radio, Track, UniqueList, is_track -from .streamdetails import StreamDetails - - -@dataclass -class QueueItem(DataClassDictMixin): - """Representation of a queue item.""" - - queue_id: str - queue_item_id: str - name: str - duration: int | None - sort_index: int = 0 - streamdetails: StreamDetails | None = None - media_item: Track | Radio | None = None - image: MediaItemImage | None = None - index: int = 0 - - def __post_init__(self) -> None: - """Set default values.""" - if not self.name and self.streamdetails and self.streamdetails.stream_title: - self.name = self.streamdetails.stream_title - if not self.name: - self.name = self.uri - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - # Exclude internal streamdetails fields from dict - if streamdetails := d.get("streamdetails"): - streamdetails.pop("data", None) - streamdetails.pop("direct", None) - streamdetails.pop("expires", None) - streamdetails.pop("path", None) - streamdetails.pop("decryption_key", None) - return d - - @property - def uri(self) -> str: - """Return uri for this QueueItem (for logging purposes).""" - if self.media_item and self.media_item.uri: - return self.media_item.uri - return self.queue_item_id - - @property - def media_type(self) -> MediaType: - """Return MediaType for this QueueItem (for convenience purposes).""" - if self.media_item: - return self.media_item.media_type - if self.streamdetails: - return self.streamdetails.media_type - return MediaType.UNKNOWN - - @classmethod - def from_media_item(cls, queue_id: str, media_item: Track | Radio) -> QueueItem: - """Construct QueueItem from track/radio item.""" - if is_track(media_item): - artists = "/".join(x.name for x in media_item.artists) - name = f"{artists} - {media_item.name}" - if media_item.version: - name = f"{name} ({media_item.version})" - # save a lot of data/bandwidth by simplifying nested objects - media_item.artists = UniqueList([ItemMapping.from_item(x) for x in media_item.artists]) - if media_item.album: - media_item.album = ItemMapping.from_item(media_item.album) - else: - name = media_item.name - return cls( - queue_id=queue_id, - queue_item_id=uuid4().hex, - name=name, - duration=media_item.duration, - media_item=media_item, - image=get_image(media_item), - ) - - def to_cache(self) -> dict[str, Any]: - """Return the dict that is suitable for storing into the cache db.""" - base = self.to_dict() - base.pop("streamdetails", None) - return base - - @classmethod - def from_cache(cls, d: dict[Any, Any]) -> Self: - """Restore a QueueItem from a cache dict.""" - d.pop("streamdetails", None) - return cls.from_dict(d) - - -def get_image(media_item: Track | Radio | None) -> MediaItemImage | None: - """Find the Image for the MediaItem.""" - if not media_item: - return None - if media_item.image: - return media_item.image - if media_item.media_type == MediaType.TRACK and (album := getattr(media_item, "album", None)): - return get_image(album) - return None diff --git a/music_assistant/common/models/streamdetails.py b/music_assistant/common/models/streamdetails.py deleted file mode 100644 index 9740e9ce6..000000000 --- a/music_assistant/common/models/streamdetails.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Model(s) for streamdetails.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any - -from mashumaro import DataClassDictMixin - -from music_assistant.common.models.enums import MediaType, StreamType, VolumeNormalizationMode -from music_assistant.common.models.media_items import AudioFormat - - -@dataclass(kw_only=True) -class StreamDetails(DataClassDictMixin): - """Model for streamdetails.""" - - # NOTE: the actual provider/itemid of the streamdetails may differ - # from the connected media_item due to track linking etc. - # the streamdetails are only used to provide details about the content - # that is going to be streamed. - - # mandatory fields - provider: str - item_id: str - audio_format: AudioFormat - media_type: MediaType = MediaType.TRACK - stream_type: StreamType = StreamType.CUSTOM - path: str | None = None - decryption_key: str | None = None - - # stream_title: radio streams can optionally set this field - stream_title: str | None = None - # duration of the item to stream, copied from media_item if omitted - duration: int | None = None - # total size in bytes of the item, calculated at eof when omitted - size: int | None = None - # data: provider specific data (not exposed externally) - # this info is for example used to pass details to the get_audio_stream - data: Any = None - # can_seek: bool to indicate that the providers 'get_audio_stream' supports seeking of the item - can_seek: bool = True - - # the fields below will be set/controlled by the streamcontroller - seek_position: int = 0 - fade_in: bool = False - loudness: float | None = None - loudness_album: float | None = None - prefer_album_loudness: bool = False - volume_normalization_mode: VolumeNormalizationMode | None = None - queue_id: str | None = None - seconds_streamed: float | None = None - target_loudness: float | None = None - strip_silence_begin: bool = False - strip_silence_end: bool = False - stream_error: bool | None = None - - def __str__(self) -> str: - """Return pretty printable string of object.""" - return self.uri - - def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]: - """Execute action(s) on serialization.""" - d.pop("queue_id", None) - d.pop("seconds_streamed", None) - d.pop("seek_position", None) - d.pop("fade_in", None) - return d - - @property - def uri(self) -> str: - """Return uri representation of item.""" - return f"{self.provider}://{self.media_type.value}/{self.item_id}" diff --git a/music_assistant/constants.py b/music_assistant/constants.py index b78e4c84e..57e53c01a 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -3,6 +3,8 @@ import pathlib from typing import Final +from music_assistant_models.config_entries import ConfigEntry, ConfigEntryType, ConfigValueOption + API_SCHEMA_VERSION: Final[int] = 26 MIN_SCHEMA_VERSION: Final[int] = 24 @@ -24,8 +26,6 @@ VARIOUS_ARTISTS_FANART: Final[str] = str(RESOURCES_DIR.joinpath("fallback_fanart.jpeg")) MASS_LOGO: Final[str] = str(RESOURCES_DIR.joinpath("logo.png")) -# if duration is None (e.g. radio stream):Final[str] = 48 hours -FALLBACK_DURATION: Final[int] = 172800 # config keys CONF_SERVER_ID: Final[str] = "server_id" @@ -98,7 +98,6 @@ "https://github.com/home-assistant/brands/raw/master/custom_integrations/mass/icon%402x.png" ) ENCRYPT_SUFFIX = "_encrypted_" -SECURE_STRING_SUBSTITUTE = "this_value_is_encrypted" CONFIGURABLE_CORE_CONTROLLERS = ( "streams", "webserver", @@ -110,3 +109,363 @@ ) VERBOSE_LOG_LEVEL: Final[int] = 5 PROVIDERS_WITH_SHAREABLE_URLS = ("spotify", "qobuz") + + +####### REUSABLE CONFIG ENTRIES ####### + +CONF_ENTRY_LOG_LEVEL = ConfigEntry( + key=CONF_LOG_LEVEL, + type=ConfigEntryType.STRING, + label="Log level", + options=( + ConfigValueOption("global", "GLOBAL"), + ConfigValueOption("info", "INFO"), + ConfigValueOption("warning", "WARNING"), + ConfigValueOption("error", "ERROR"), + ConfigValueOption("debug", "DEBUG"), + ConfigValueOption("verbose", "VERBOSE"), + ), + default_value="GLOBAL", + category="advanced", +) + +DEFAULT_PROVIDER_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) +DEFAULT_CORE_CONFIG_ENTRIES = (CONF_ENTRY_LOG_LEVEL,) + +# some reusable player config entries + +CONF_ENTRY_FLOW_MODE = ConfigEntry( + key=CONF_FLOW_MODE, + type=ConfigEntryType.BOOLEAN, + label="Enable queue flow mode", + default_value=False, +) + +CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED = ConfigEntry.from_dict( + {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True} +) + +CONF_ENTRY_FLOW_MODE_ENFORCED = ConfigEntry.from_dict( + {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": True, "value": True, "hidden": True} +) + +CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED = ConfigEntry.from_dict( + {**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": False, "value": False, "hidden": True} +) + + +CONF_ENTRY_AUTO_PLAY = ConfigEntry( + key=CONF_AUTO_PLAY, + type=ConfigEntryType.BOOLEAN, + label="Automatically play/resume on power on", + default_value=False, + description="When this player is turned ON, automatically start playing " + "(if there are items in the queue).", +) + +CONF_ENTRY_OUTPUT_CHANNELS = ConfigEntry( + key=CONF_OUTPUT_CHANNELS, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Stereo (both channels)", "stereo"), + ConfigValueOption("Left channel", "left"), + ConfigValueOption("Right channel", "right"), + ConfigValueOption("Mono (both channels)", "mono"), + ), + default_value="stereo", + label="Output Channel Mode", + category="audio", +) + +CONF_ENTRY_VOLUME_NORMALIZATION = ConfigEntry( + key=CONF_VOLUME_NORMALIZATION, + type=ConfigEntryType.BOOLEAN, + label="Enable volume normalization", + default_value=True, + description="Enable volume normalization (EBU-R128 based)", + category="audio", +) + +CONF_ENTRY_VOLUME_NORMALIZATION_TARGET = ConfigEntry( + key=CONF_VOLUME_NORMALIZATION_TARGET, + type=ConfigEntryType.INTEGER, + range=(-70, -5), + default_value=-17, + label="Target level for volume normalization", + description="Adjust average (perceived) loudness to this target level", + depends_on=CONF_VOLUME_NORMALIZATION, + category="advanced", +) + +CONF_ENTRY_EQ_BASS = ConfigEntry( + key=CONF_EQ_BASS, + type=ConfigEntryType.INTEGER, + range=(-10, 10), + default_value=0, + label="Equalizer: bass", + description="Use the builtin basic equalizer to adjust the bass of audio.", + category="audio", +) + +CONF_ENTRY_EQ_MID = ConfigEntry( + key=CONF_EQ_MID, + type=ConfigEntryType.INTEGER, + range=(-10, 10), + default_value=0, + label="Equalizer: midrange", + description="Use the builtin basic equalizer to adjust the midrange of audio.", + category="audio", +) + +CONF_ENTRY_EQ_TREBLE = ConfigEntry( + key=CONF_EQ_TREBLE, + type=ConfigEntryType.INTEGER, + range=(-10, 10), + default_value=0, + label="Equalizer: treble", + description="Use the builtin basic equalizer to adjust the treble of audio.", + category="audio", +) + + +CONF_ENTRY_CROSSFADE = ConfigEntry( + key=CONF_CROSSFADE, + type=ConfigEntryType.BOOLEAN, + label="Enable crossfade", + default_value=False, + description="Enable a crossfade transition between (queue) tracks.", + category="audio", +) + +CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED = ConfigEntry( + key=CONF_CROSSFADE, + type=ConfigEntryType.BOOLEAN, + label="Enable crossfade", + default_value=False, + description="Enable a crossfade transition between (queue) tracks.\n\n " + "Requires flow-mode to be enabled", + category="audio", + depends_on=CONF_FLOW_MODE, +) + +CONF_ENTRY_CROSSFADE_DURATION = ConfigEntry( + key=CONF_CROSSFADE_DURATION, + type=ConfigEntryType.INTEGER, + range=(1, 10), + default_value=8, + label="Crossfade duration", + description="Duration in seconds of the crossfade between tracks (if enabled)", + depends_on=CONF_CROSSFADE, + category="advanced", +) + +CONF_ENTRY_HIDE_PLAYER = ConfigEntry( + key=CONF_HIDE_PLAYER, + type=ConfigEntryType.BOOLEAN, + label="Hide this player in the user interface", + default_value=False, +) + +CONF_ENTRY_ENFORCE_MP3 = ConfigEntry( + key=CONF_ENFORCE_MP3, + type=ConfigEntryType.BOOLEAN, + label="Enforce (lossy) mp3 stream", + default_value=False, + description="By default, Music Assistant sends lossless, high quality audio " + "to all players. Some players can not deal with that and require the stream to be packed " + "into a lossy mp3 codec. \n\n " + "Only enable when needed. Saves some bandwidth at the cost of audio quality.", + category="audio", +) + +CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED = ConfigEntry.from_dict( + {**CONF_ENTRY_ENFORCE_MP3.to_dict(), "default_value": True} +) + +CONF_ENTRY_SYNC_ADJUST = ConfigEntry( + key=CONF_SYNC_ADJUST, + type=ConfigEntryType.INTEGER, + range=(-500, 500), + default_value=0, + label="Audio synchronization delay correction", + description="If this player is playing audio synced with other players " + "and you always hear the audio too early or late on this player, " + "you can shift the audio a bit.", + category="advanced", +) + + +CONF_ENTRY_TTS_PRE_ANNOUNCE = ConfigEntry( + key=CONF_TTS_PRE_ANNOUNCE, + type=ConfigEntryType.BOOLEAN, + default_value=True, + label="Pre-announce TTS announcements", + category="announcements", +) + + +CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME_STRATEGY, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Absolute volume", "absolute"), + ConfigValueOption("Relative volume increase", "relative"), + ConfigValueOption("Volume increase by fixed percentage", "percentual"), + ConfigValueOption("Do not adjust volume", "none"), + ), + default_value="percentual", + label="Volume strategy for Announcements", + category="announcements", +) + +CONF_ENTRY_ANNOUNCE_VOLUME = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME, + type=ConfigEntryType.INTEGER, + default_value=85, + label="Volume for Announcements", + category="announcements", +) + +CONF_ENTRY_ANNOUNCE_VOLUME_MIN = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME_MIN, + type=ConfigEntryType.INTEGER, + default_value=15, + label="Minimum Volume level for Announcements", + description="The volume (adjustment) of announcements should no go below this level.", + category="announcements", +) + +CONF_ENTRY_ANNOUNCE_VOLUME_MAX = ConfigEntry( + key=CONF_ANNOUNCE_VOLUME_MAX, + type=ConfigEntryType.INTEGER, + default_value=75, + label="Maximum Volume level for Announcements", + description="The volume (adjustment) of announcements should no go above this level.", + category="announcements", +) + +CONF_ENTRY_PLAYER_ICON = ConfigEntry( + key=CONF_ICON, + type=ConfigEntryType.ICON, + default_value="mdi-speaker", + label="Icon", + description="Material design icon for this player. " + "\n\nSee https://pictogrammers.com/library/mdi/", + category="generic", +) + +CONF_ENTRY_PLAYER_ICON_GROUP = ConfigEntry.from_dict( + {**CONF_ENTRY_PLAYER_ICON.to_dict(), "default_value": "mdi-speaker-multiple"} +) + +CONF_ENTRY_SAMPLE_RATES = ConfigEntry( + key=CONF_SAMPLE_RATES, + type=ConfigEntryType.INTEGER_TUPLE, + options=( + ConfigValueOption("44.1kHz / 16 bits", (44100, 16)), + ConfigValueOption("44.1kHz / 24 bits", (44100, 24)), + ConfigValueOption("48kHz / 16 bits", (48000, 16)), + ConfigValueOption("48kHz / 24 bits", (48000, 24)), + ConfigValueOption("88.2kHz / 16 bits", (88200, 16)), + ConfigValueOption("88.2kHz / 24 bits", (88200, 24)), + ConfigValueOption("96kHz / 16 bits", (96000, 16)), + ConfigValueOption("96kHz / 24 bits", (96000, 24)), + ConfigValueOption("176.4kHz / 16 bits", (176400, 16)), + ConfigValueOption("176.4kHz / 24 bits", (176400, 24)), + ConfigValueOption("192kHz / 16 bits", (192000, 16)), + ConfigValueOption("192kHz / 24 bits", (192000, 24)), + ConfigValueOption("352.8kHz / 16 bits", (352800, 16)), + ConfigValueOption("352.8kHz / 24 bits", (352800, 24)), + ConfigValueOption("384kHz / 16 bits", (384000, 16)), + ConfigValueOption("384kHz / 24 bits", (384000, 24)), + ), + default_value=[(44100, 16), (48000, 16)], + required=True, + multi_value=True, + label="Sample rates supported by this player", + category="advanced", + description="The sample rates (and bit depths) supported by this player.\n" + "Content with unsupported sample rates will be automatically resampled.", +) + + +CONF_ENTRY_HTTP_PROFILE = ConfigEntry( + key=CONF_HTTP_PROFILE, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Profile 1 - chunked", "chunked"), + ConfigValueOption("Profile 2 - no content length", "no_content_length"), + ConfigValueOption("Profile 3 - forced content length", "forced_content_length"), + ), + default_value="no_content_length", + label="HTTP Profile used for sending audio", + category="advanced", + description="This is considered to be a very advanced setting, only adjust this if needed, " + "for example if your player stops playing halfway streams or if you experience " + "other playback related issues. In most cases the default setting is fine.", +) + +CONF_ENTRY_HTTP_PROFILE_FORCED_1 = ConfigEntry.from_dict( + {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "chunked", "hidden": True} +) +CONF_ENTRY_HTTP_PROFILE_FORCED_2 = ConfigEntry.from_dict( + {**CONF_ENTRY_HTTP_PROFILE.to_dict(), "default_value": "no_content_length", "hidden": True} +) + +CONF_ENTRY_ENABLE_ICY_METADATA = ConfigEntry( + key=CONF_ENABLE_ICY_METADATA, + type=ConfigEntryType.STRING, + options=( + ConfigValueOption("Disabled - do not send ICY metadata", "disabled"), + ConfigValueOption("Profile 1 - basic info", "basic"), + ConfigValueOption("Profile 2 - full info (including image)", "full"), + ), + depends_on=CONF_FLOW_MODE, + default_value="disabled", + label="Try to ingest metadata into stream (ICY)", + category="advanced", + description="Try to ingest metadata into the stream (ICY) to show track info on the player, " + "even when flow mode is enabled.\n\nThis is called ICY metadata and its what is also used by " + "online radio station to inform you what is playing. \n\nBe aware that not all players support " + "this correctly. If you experience issues with playback, try to disable this setting.", +) + + +def create_sample_rates_config_entry( + max_sample_rate: int, + max_bit_depth: int, + safe_max_sample_rate: int = 48000, + safe_max_bit_depth: int = 16, + hidden: bool = False, +) -> ConfigEntry: + """Create sample rates config entry based on player specific helpers.""" + assert CONF_ENTRY_SAMPLE_RATES.options + conf_entry = ConfigEntry.from_dict(CONF_ENTRY_SAMPLE_RATES.to_dict()) + conf_entry.hidden = hidden + options: list[ConfigValueOption] = [] + default_value: list[tuple[int, int]] = [] + for option in CONF_ENTRY_SAMPLE_RATES.options: + if not isinstance(option.value, tuple): + continue + sample_rate, bit_depth = option.value + if sample_rate <= max_sample_rate and bit_depth <= max_bit_depth: + options.append(option) + if sample_rate <= safe_max_sample_rate and bit_depth <= safe_max_bit_depth: + default_value.append(option.value) + conf_entry.options = tuple(options) + conf_entry.default_value = default_value + return conf_entry + + +BASE_PLAYER_CONFIG_ENTRIES = ( + # config entries that are valid for all players + CONF_ENTRY_PLAYER_ICON, + CONF_ENTRY_FLOW_MODE, + CONF_ENTRY_VOLUME_NORMALIZATION, + CONF_ENTRY_AUTO_PLAY, + CONF_ENTRY_VOLUME_NORMALIZATION_TARGET, + CONF_ENTRY_HIDE_PLAYER, + CONF_ENTRY_TTS_PRE_ANNOUNCE, + CONF_ENTRY_SAMPLE_RATES, + CONF_ENTRY_HTTP_PROFILE_FORCED_2, +) diff --git a/music_assistant/server/controllers/__init__.py b/music_assistant/controllers/__init__.py similarity index 100% rename from music_assistant/server/controllers/__init__.py rename to music_assistant/controllers/__init__.py diff --git a/music_assistant/server/controllers/cache.py b/music_assistant/controllers/cache.py similarity index 97% rename from music_assistant/server/controllers/cache.py rename to music_assistant/controllers/cache.py index 5e29edd74..687b33747 100644 --- a/music_assistant/server/controllers/cache.py +++ b/music_assistant/controllers/cache.py @@ -11,15 +11,16 @@ from collections.abc import Callable, Iterator, MutableMapping from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar -from music_assistant.common.helpers.json import json_dumps, json_loads -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType + from music_assistant.constants import DB_TABLE_CACHE, DB_TABLE_SETTINGS, MASS_LOGGER_NAME -from music_assistant.server.helpers.database import DatabaseConnection -from music_assistant.server.models.core_controller import CoreController +from music_assistant.helpers.database import DatabaseConnection +from music_assistant.helpers.json import json_dumps, json_loads +from music_assistant.models.core_controller import CoreController if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig + from music_assistant_models.config_entries import CoreConfig LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.cache") CONF_CLEAR_CACHE = "clear_cache" diff --git a/music_assistant/server/controllers/config.py b/music_assistant/controllers/config.py similarity index 98% rename from music_assistant/server/controllers/config.py rename to music_assistant/controllers/config.py index d77462a6c..f8bfea0c6 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/controllers/config.py @@ -13,11 +13,8 @@ import shortuuid from aiofiles.os import wrap from cryptography.fernet import Fernet, InvalidToken - -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads -from music_assistant.common.models import config_entries -from music_assistant.common.models.config_entries import ( +from music_assistant_models import config_entries +from music_assistant_models.config_entries import ( DEFAULT_CORE_CONFIG_ENTRIES, DEFAULT_PROVIDER_CONFIG_ENTRIES, ConfigEntry, @@ -26,13 +23,15 @@ PlayerConfig, ProviderConfig, ) -from music_assistant.common.models.enums import EventType, ProviderFeature, ProviderType -from music_assistant.common.models.errors import ( +from music_assistant_models.enums import EventType, ProviderFeature, ProviderType +from music_assistant_models.errors import ( ActionUnavailable, InvalidDataError, PlayerCommandFailed, UnsupportedFeaturedException, ) +from music_assistant_models.helpers.global_cache import get_global_cache_value + from music_assistant.constants import ( CONF_CORE, CONF_PLAYERS, @@ -41,14 +40,15 @@ CONFIGURABLE_CORE_CONTROLLERS, ENCRYPT_SUFFIX, ) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.util import load_provider_module +from music_assistant.helpers.api import api_command +from music_assistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads +from music_assistant.helpers.util import load_provider_module if TYPE_CHECKING: import asyncio - from music_assistant.server.models.core_controller import CoreController - from music_assistant.server.server import MusicAssistant + from music_assistant import MusicAssistant + from music_assistant.models.core_controller import CoreController LOGGER = logging.getLogger(__name__) DEFAULT_SAVE_DELAY = 5 diff --git a/music_assistant/server/controllers/media/__init__.py b/music_assistant/controllers/media/__init__.py similarity index 100% rename from music_assistant/server/controllers/media/__init__.py rename to music_assistant/controllers/media/__init__.py diff --git a/music_assistant/server/controllers/media/albums.py b/music_assistant/controllers/media/albums.py similarity index 97% rename from music_assistant/server/controllers/media/albums.py rename to music_assistant/controllers/media/albums.py index a2733168e..b0cfb8568 100644 --- a/music_assistant/server/controllers/media/albums.py +++ b/music_assistant/controllers/media/albums.py @@ -6,14 +6,13 @@ from collections.abc import Iterable from typing import TYPE_CHECKING, Any -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import CacheCategory, ProviderFeature -from music_assistant.common.models.errors import ( +from music_assistant_models.enums import CacheCategory, ProviderFeature +from music_assistant_models.errors import ( InvalidDataError, MediaNotFoundError, UnsupportedFeaturedException, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -22,17 +21,19 @@ Track, UniqueList, ) + from music_assistant.constants import DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ALBUM_TRACKS, DB_TABLE_ALBUMS -from music_assistant.server.controllers.media.base import MediaControllerBase -from music_assistant.server.helpers.compare import ( +from music_assistant.controllers.media.base import MediaControllerBase +from music_assistant.helpers.compare import ( compare_album, compare_artists, compare_media_item, loose_compare_strings, ) +from music_assistant.helpers.json import serialize_to_json if TYPE_CHECKING: - from music_assistant.server.models.music_provider import MusicProvider + from music_assistant.models.music_provider import MusicProvider class AlbumsController(MediaControllerBase[Album]): diff --git a/music_assistant/server/controllers/media/artists.py b/music_assistant/controllers/media/artists.py similarity index 97% rename from music_assistant/server/controllers/media/artists.py rename to music_assistant/controllers/media/artists.py index ce419192b..776d907ac 100644 --- a/music_assistant/server/controllers/media/artists.py +++ b/music_assistant/controllers/media/artists.py @@ -6,14 +6,13 @@ import contextlib from typing import TYPE_CHECKING, Any -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import CacheCategory, ProviderFeature -from music_assistant.common.models.errors import ( +from music_assistant_models.enums import CacheCategory, ProviderFeature +from music_assistant_models.errors import ( MediaNotFoundError, ProviderUnavailableError, UnsupportedFeaturedException, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -22,6 +21,7 @@ Track, UniqueList, ) + from music_assistant.constants import ( DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ARTISTS, @@ -29,11 +29,12 @@ VARIOUS_ARTISTS_MBID, VARIOUS_ARTISTS_NAME, ) -from music_assistant.server.controllers.media.base import MediaControllerBase -from music_assistant.server.helpers.compare import compare_artist, compare_strings +from music_assistant.controllers.media.base import MediaControllerBase +from music_assistant.helpers.compare import compare_artist, compare_strings +from music_assistant.helpers.json import serialize_to_json if TYPE_CHECKING: - from music_assistant.server.models.music_provider import MusicProvider + from music_assistant.models.music_provider import MusicProvider class ArtistsController(MediaControllerBase[Artist]): diff --git a/music_assistant/server/controllers/media/base.py b/music_assistant/controllers/media/base.py similarity index 98% rename from music_assistant/server/controllers/media/base.py rename to music_assistant/controllers/media/base.py index ddad990d1..5fc7b8b04 100644 --- a/music_assistant/server/controllers/media/base.py +++ b/music_assistant/controllers/media/base.py @@ -9,16 +9,15 @@ from contextlib import suppress from typing import TYPE_CHECKING, Any, Generic, TypeVar -from music_assistant.common.helpers.json import json_loads, serialize_to_json -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( CacheCategory, EventType, ExternalID, MediaType, ProviderFeature, ) -from music_assistant.common.models.errors import MediaNotFoundError, ProviderUnavailableError -from music_assistant.common.models.media_items import ( +from music_assistant_models.errors import MediaNotFoundError, ProviderUnavailableError +from music_assistant_models.media_items import ( Album, ItemMapping, MediaItemType, @@ -26,13 +25,15 @@ SearchResults, Track, ) + from music_assistant.constants import DB_TABLE_PLAYLOG, DB_TABLE_PROVIDER_MAPPINGS, MASS_LOGGER_NAME -from music_assistant.server.helpers.compare import compare_media_item +from music_assistant.helpers.compare import compare_media_item +from music_assistant.helpers.json import json_loads, serialize_to_json if TYPE_CHECKING: from collections.abc import AsyncGenerator, Mapping - from music_assistant.server import MusicAssistant + from music_assistant import MusicAssistant ItemCls = TypeVar("ItemCls", bound="MediaItemType") diff --git a/music_assistant/server/controllers/media/playlists.py b/music_assistant/controllers/media/playlists.py similarity index 97% rename from music_assistant/server/controllers/media/playlists.py rename to music_assistant/controllers/media/playlists.py index e158eb277..5051c7433 100644 --- a/music_assistant/server/controllers/media/playlists.py +++ b/music_assistant/controllers/media/playlists.py @@ -7,23 +7,19 @@ from collections.abc import AsyncGenerator from typing import Any -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.helpers.uri import create_uri, parse_uri -from music_assistant.common.models.enums import ( - CacheCategory, - MediaType, - ProviderFeature, - ProviderType, -) -from music_assistant.common.models.errors import ( +from music_assistant_models.enums import CacheCategory, MediaType, ProviderFeature, ProviderType +from music_assistant_models.errors import ( InvalidDataError, MediaNotFoundError, ProviderUnavailableError, UnsupportedFeaturedException, ) -from music_assistant.common.models.media_items import Playlist, PlaylistTrack, Track +from music_assistant_models.helpers.uri import create_uri, parse_uri +from music_assistant_models.media_items import Playlist, PlaylistTrack, Track + from music_assistant.constants import DB_TABLE_PLAYLISTS -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.json import serialize_to_json +from music_assistant.models.music_provider import MusicProvider from .base import MediaControllerBase diff --git a/music_assistant/server/controllers/media/radio.py b/music_assistant/controllers/media/radio.py similarity index 94% rename from music_assistant/server/controllers/media/radio.py rename to music_assistant/controllers/media/radio.py index b8f391d22..d16e71e88 100644 --- a/music_assistant/server/controllers/media/radio.py +++ b/music_assistant/controllers/media/radio.py @@ -4,11 +4,12 @@ import asyncio -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import MediaType -from music_assistant.common.models.media_items import Radio, Track +from music_assistant_models.enums import MediaType +from music_assistant_models.media_items import Radio, Track + from music_assistant.constants import DB_TABLE_RADIOS -from music_assistant.server.helpers.compare import loose_compare_strings +from music_assistant.helpers.compare import loose_compare_strings +from music_assistant.helpers.json import serialize_to_json from .base import MediaControllerBase diff --git a/music_assistant/server/controllers/media/tracks.py b/music_assistant/controllers/media/tracks.py similarity index 98% rename from music_assistant/server/controllers/media/tracks.py rename to music_assistant/controllers/media/tracks.py index 5ea9cd3df..0fc1523a4 100644 --- a/music_assistant/server/controllers/media/tracks.py +++ b/music_assistant/controllers/media/tracks.py @@ -7,15 +7,14 @@ from contextlib import suppress from typing import Any -from music_assistant.common.helpers.json import serialize_to_json -from music_assistant.common.models.enums import MediaType, ProviderFeature -from music_assistant.common.models.errors import ( +from music_assistant_models.enums import MediaType, ProviderFeature +from music_assistant_models.errors import ( InvalidDataError, MediaNotFoundError, MusicAssistantError, UnsupportedFeaturedException, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, Artist, ItemMapping, @@ -23,19 +22,21 @@ Track, UniqueList, ) + from music_assistant.constants import ( DB_TABLE_ALBUM_TRACKS, DB_TABLE_ALBUMS, DB_TABLE_TRACK_ARTISTS, DB_TABLE_TRACKS, ) -from music_assistant.server.helpers.compare import ( +from music_assistant.helpers.compare import ( compare_artists, compare_media_item, compare_track, loose_compare_strings, ) -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.json import serialize_to_json +from music_assistant.models.music_provider import MusicProvider from .base import MediaControllerBase diff --git a/music_assistant/server/controllers/metadata.py b/music_assistant/controllers/metadata.py similarity index 97% rename from music_assistant/server/controllers/metadata.py rename to music_assistant/controllers/metadata.py index 8140784e8..2c69820e6 100644 --- a/music_assistant/server/controllers/metadata.py +++ b/music_assistant/controllers/metadata.py @@ -16,14 +16,8 @@ import aiofiles from aiohttp import web - -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( AlbumType, ConfigEntryType, ImageType, @@ -31,8 +25,9 @@ ProviderFeature, ProviderType, ) -from music_assistant.common.models.errors import MediaNotFoundError, ProviderUnavailableError -from music_assistant.common.models.media_items import ( +from music_assistant_models.errors import MediaNotFoundError, ProviderUnavailableError +from music_assistant_models.helpers.global_cache import get_global_cache_value +from music_assistant_models.media_items import ( Album, Artist, ItemMapping, @@ -41,6 +36,7 @@ Playlist, Track, ) + from music_assistant.constants import ( CONF_LANGUAGE, DB_TABLE_ALBUMS, @@ -50,16 +46,17 @@ VARIOUS_ARTISTS_NAME, VERBOSE_LOG_LEVEL, ) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.compare import compare_strings -from music_assistant.server.helpers.images import create_collage, get_image_thumb -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.core_controller import CoreController +from music_assistant.helpers.api import api_command +from music_assistant.helpers.compare import compare_strings +from music_assistant.helpers.images import create_collage, get_image_thumb +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.core_controller import CoreController if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig - from music_assistant.server.models.metadata_provider import MetadataProvider - from music_assistant.server.providers.musicbrainz import MusicbrainzProvider + from music_assistant_models.config_entries import CoreConfig + + from music_assistant.models.metadata_provider import MetadataProvider + from music_assistant.providers.musicbrainz import MusicbrainzProvider LOCALES = { "af_ZA": "African", diff --git a/music_assistant/server/controllers/music.py b/music_assistant/controllers/music.py similarity index 98% rename from music_assistant/server/controllers/music.py rename to music_assistant/controllers/music.py index 1d1f0e293..b8c92578d 100644 --- a/music_assistant/server/controllers/music.py +++ b/music_assistant/controllers/music.py @@ -11,11 +11,8 @@ from math import inf from typing import TYPE_CHECKING, Final, cast -from music_assistant.common.helpers.datetime import utc_timestamp -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.helpers.uri import parse_uri -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ( CacheCategory, ConfigEntryType, EventType, @@ -23,20 +20,23 @@ ProviderFeature, ProviderType, ) -from music_assistant.common.models.errors import ( +from music_assistant_models.errors import ( InvalidProviderID, InvalidProviderURI, MediaNotFoundError, MusicAssistantError, ProviderUnavailableError, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.helpers.global_cache import get_global_cache_value +from music_assistant_models.helpers.uri import parse_uri +from music_assistant_models.media_items import ( BrowseFolder, ItemMapping, MediaItemType, SearchResults, ) -from music_assistant.common.models.provider import ProviderInstance, SyncTask +from music_assistant_models.provider import ProviderInstance, SyncTask + from music_assistant.constants import ( DB_TABLE_ALBUM_ARTISTS, DB_TABLE_ALBUM_TRACKS, @@ -52,10 +52,11 @@ DB_TABLE_TRACKS, PROVIDERS_WITH_SHAREABLE_URLS, ) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.database import DatabaseConnection -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.core_controller import CoreController +from music_assistant.helpers.api import api_command +from music_assistant.helpers.database import DatabaseConnection +from music_assistant.helpers.datetime import utc_timestamp +from music_assistant.helpers.util import TaskManager +from music_assistant.models.core_controller import CoreController from .media.albums import AlbumsController from .media.artists import ArtistsController @@ -64,8 +65,9 @@ from .media.tracks import TracksController if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig - from music_assistant.server.models.music_provider import MusicProvider + from music_assistant_models.config_entries import CoreConfig + + from music_assistant.models.music_provider import MusicProvider CONF_RESET_DB = "reset_db" DEFAULT_SYNC_INTERVAL = 3 * 60 # default sync interval in minutes diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/controllers/player_queues.py similarity index 98% rename from music_assistant/server/controllers/player_queues.py rename to music_assistant/controllers/player_queues.py index 68eb7941c..f605d69e7 100644 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -18,13 +18,8 @@ import time from typing import TYPE_CHECKING, Any, TypedDict -from music_assistant.common.helpers.util import get_changed_keys -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( CacheCategory, ConfigEntryType, EventType, @@ -34,7 +29,7 @@ QueueOption, RepeatMode, ) -from music_assistant.common.models.errors import ( +from music_assistant_models.errors import ( InvalidCommand, MediaNotFoundError, MusicAssistantError, @@ -42,27 +37,24 @@ QueueEmpty, UnsupportedFeaturedException, ) -from music_assistant.common.models.media_items import ( - AudioFormat, - MediaItemType, - Playlist, - media_from_dict, -) -from music_assistant.common.models.player import PlayerMedia -from music_assistant.common.models.player_queue import PlayerQueue -from music_assistant.common.models.queue_item import QueueItem -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.media_items import AudioFormat, MediaItemType, Playlist, media_from_dict +from music_assistant_models.player import PlayerMedia +from music_assistant_models.player_queue import PlayerQueue +from music_assistant_models.queue_item import QueueItem +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import CONF_CROSSFADE, CONF_FLOW_MODE, MASS_LOGO_ONLINE -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.audio import get_stream_details -from music_assistant.server.helpers.throttle_retry import BYPASS_THROTTLER -from music_assistant.server.models.core_controller import CoreController +from music_assistant.helpers.api import api_command +from music_assistant.helpers.audio import get_stream_details +from music_assistant.helpers.throttle_retry import BYPASS_THROTTLER +from music_assistant.helpers.util import get_changed_keys +from music_assistant.models.core_controller import CoreController if TYPE_CHECKING: from collections.abc import Iterator - from music_assistant.common.models.media_items import Album, Artist, Track - from music_assistant.common.models.player import Player + from music_assistant_models.media_items import Album, Artist, Track + from music_assistant_models.player import Player CONF_DEFAULT_ENQUEUE_SELECT_ARTIST = "default_enqueue_select_artist" diff --git a/music_assistant/server/controllers/players.py b/music_assistant/controllers/players.py similarity index 98% rename from music_assistant/server/controllers/players.py rename to music_assistant/controllers/players.py index 8bf6e3ced..24c8d282d 100644 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/controllers/players.py @@ -14,17 +14,7 @@ from contextlib import suppress from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, cast -from music_assistant.common.helpers.util import get_changed_values -from music_assistant.common.models.config_entries import ( - CONF_ENTRY_ANNOUNCE_VOLUME, - CONF_ENTRY_ANNOUNCE_VOLUME_MAX, - CONF_ENTRY_ANNOUNCE_VOLUME_MIN, - CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, - CONF_ENTRY_PLAYER_ICON, - CONF_ENTRY_PLAYER_ICON_GROUP, - PlayerConfig, -) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( EventType, MediaType, PlayerFeature, @@ -32,32 +22,39 @@ PlayerType, ProviderType, ) -from music_assistant.common.models.errors import ( +from music_assistant_models.errors import ( AlreadyRegisteredError, PlayerCommandFailed, PlayerUnavailableError, UnsupportedFeaturedException, ) -from music_assistant.common.models.media_items import UniqueList -from music_assistant.common.models.player import Player, PlayerMedia +from music_assistant_models.media_items import UniqueList +from music_assistant_models.player import Player, PlayerMedia + from music_assistant.constants import ( CONF_AUTO_PLAY, + CONF_ENTRY_ANNOUNCE_VOLUME, + CONF_ENTRY_ANNOUNCE_VOLUME_MAX, + CONF_ENTRY_ANNOUNCE_VOLUME_MIN, + CONF_ENTRY_ANNOUNCE_VOLUME_STRATEGY, + CONF_ENTRY_PLAYER_ICON, + CONF_ENTRY_PLAYER_ICON_GROUP, CONF_HIDE_PLAYER, CONF_PLAYERS, CONF_TTS_PRE_ANNOUNCE, ) -from music_assistant.server.helpers.api import api_command -from music_assistant.server.helpers.tags import parse_tags -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.core_controller import CoreController -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.player_group import PlayerGroupProvider +from music_assistant.helpers.api import api_command +from music_assistant.helpers.tags import parse_tags +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.helpers.util import TaskManager, get_changed_values +from music_assistant.models.core_controller import CoreController +from music_assistant.models.player_provider import PlayerProvider +from music_assistant.providers.player_group import PlayerGroupProvider if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Coroutine, Iterator - from music_assistant.common.models.config_entries import CoreConfig + from music_assistant_models.config_entries import CoreConfig, PlayerConfig _PlayerControllerT = TypeVar("_PlayerControllerT", bound="PlayerController") diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/controllers/streams.py similarity index 97% rename from music_assistant/server/controllers/streams.py rename to music_assistant/controllers/streams.py index c13ab501f..b760b2829 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -16,25 +16,23 @@ from aiofiles.os import wrap from aiohttp import web - -from music_assistant.common.helpers.util import get_ip, select_free_port, try_parse_bool -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_ENABLE_ICY_METADATA, ConfigEntry, ConfigValueOption, ConfigValueType, ) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ConfigEntryType, ContentType, MediaType, StreamType, VolumeNormalizationMode, ) -from music_assistant.common.models.errors import QueueEmpty -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player_queue import PlayLogEntry -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.errors import QueueEmpty +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player_queue import PlayLogEntry + from music_assistant.constants import ( ANNOUNCE_ALERT_FILE, CONF_BIND_IP, @@ -54,8 +52,8 @@ SILENCE_FILE, VERBOSE_LOG_LEVEL, ) -from music_assistant.server.helpers.audio import LOGGER as AUDIO_LOGGER -from music_assistant.server.helpers.audio import ( +from music_assistant.helpers.audio import LOGGER as AUDIO_LOGGER +from music_assistant.helpers.audio import ( check_audio_support, crossfade_pcm_parts, get_chunksize, @@ -66,17 +64,18 @@ get_silence, get_stream_details, ) -from music_assistant.server.helpers.ffmpeg import LOGGER as FFMPEG_LOGGER -from music_assistant.server.helpers.ffmpeg import get_ffmpeg_stream -from music_assistant.server.helpers.util import get_ips -from music_assistant.server.helpers.webserver import Webserver -from music_assistant.server.models.core_controller import CoreController +from music_assistant.helpers.ffmpeg import LOGGER as FFMPEG_LOGGER +from music_assistant.helpers.ffmpeg import get_ffmpeg_stream +from music_assistant.helpers.util import get_ip, get_ips, select_free_port, try_parse_bool +from music_assistant.helpers.webserver import Webserver +from music_assistant.models.core_controller import CoreController if TYPE_CHECKING: - from music_assistant.common.models.config_entries import CoreConfig - from music_assistant.common.models.player import Player - from music_assistant.common.models.player_queue import PlayerQueue - from music_assistant.common.models.queue_item import QueueItem + from music_assistant_models.config_entries import CoreConfig + from music_assistant_models.player import Player + from music_assistant_models.player_queue import PlayerQueue + from music_assistant_models.queue_item import QueueItem + from music_assistant_models.streamdetails import StreamDetails DEFAULT_STREAM_HEADERS = { diff --git a/music_assistant/server/controllers/webserver.py b/music_assistant/controllers/webserver.py similarity index 95% rename from music_assistant/server/controllers/webserver.py rename to music_assistant/controllers/webserver.py index f117f779c..29cfe9d02 100644 --- a/music_assistant/server/controllers/webserver.py +++ b/music_assistant/controllers/webserver.py @@ -18,29 +18,28 @@ from aiohttp import WSMsgType, web from music_assistant_frontend import where as locate_frontend - -from music_assistant.common.helpers.util import get_ip -from music_assistant.common.models.api import ( +from music_assistant_models.api import ( CommandMessage, ErrorResultMessage, MessageType, SuccessResultMessage, ) -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueOption -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.errors import InvalidCommand +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.errors import InvalidCommand + from music_assistant.constants import CONF_BIND_IP, CONF_BIND_PORT, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.api import APICommandHandler, parse_arguments -from music_assistant.server.helpers.audio import get_preview_stream -from music_assistant.server.helpers.util import get_ips -from music_assistant.server.helpers.webserver import Webserver -from music_assistant.server.models.core_controller import CoreController +from music_assistant.helpers.api import APICommandHandler, parse_arguments +from music_assistant.helpers.audio import get_preview_stream +from music_assistant.helpers.util import get_ip, get_ips +from music_assistant.helpers.webserver import Webserver +from music_assistant.models.core_controller import CoreController if TYPE_CHECKING: from collections.abc import Awaitable - from music_assistant.common.models.config_entries import ConfigValueType, CoreConfig - from music_assistant.common.models.event import MassEvent + from music_assistant_models.config_entries import ConfigValueType, CoreConfig + from music_assistant_models.event import MassEvent DEFAULT_SERVER_PORT = 8095 CONF_BASE_URL = "base_url" diff --git a/music_assistant/server/helpers/__init__.py b/music_assistant/helpers/__init__.py similarity index 100% rename from music_assistant/server/helpers/__init__.py rename to music_assistant/helpers/__init__.py diff --git a/music_assistant/server/helpers/api.py b/music_assistant/helpers/api.py similarity index 100% rename from music_assistant/server/helpers/api.py rename to music_assistant/helpers/api.py diff --git a/music_assistant/server/helpers/app_vars.py b/music_assistant/helpers/app_vars.py similarity index 94% rename from music_assistant/server/helpers/app_vars.py rename to music_assistant/helpers/app_vars.py index 9efefc606..2ec9bb8da 100644 --- a/music_assistant/server/helpers/app_vars.py +++ b/music_assistant/helpers/app_vars.py @@ -2,4 +2,4 @@ # fmt: off # flake8: noqa # ruff: noqa -(lambda __g: [(lambda __mod: [[[None for __g['app_var'], app_var.__name__ in [(lambda index: (lambda __l: [[AV(aap(__l['var'].encode()).decode()) for __l['var'] in [(vars.split('acb2')[__l['index']][::(-1)])]][0] for __l['index'] in [(index)]][0])({}), 'app_var')]][0] for __g['vars'] in [('3YTNyUDOyQTOacb2=EmN5M2YjdzMhljYzYzYhlDMmFGNlVTOmNDZwMzNxYzNacb2=UDMzEGOyADO1QWO5kDNygTMlJGN5QzNzIWOmZTOiVmMacb2yMTNzITNacb2=UDZhJmMldTZ3QTY4IjZ3kTNxYjN0czNwI2YxkTM5MjNacb2==QMh5WOmZnewM2d4UDblRzZacb20QzMwAjNacb2=QzNiRTO3EjMjFzMldjY3QTMwEDMwADMiNWZ5UWO3UWMacb2RJ1UJpXUPlzdvZUZ0w2VzVjMq1mblRnZvBHZ4x2RMZWbqhVS5JkdQ1WS38FVDpFTw9WcthGb41GaoV3dQV1QHNVRutUMjRFe09VeGh1RO1yQFtkZ3RnL5IERNJTVE5EerpWTzUkaPlWUYlFcKNET3FkaOlXSU9EMRpnT49maJdHaYpVa3lWS0MXVUpXSU5URWRlT1kUaPlWTzMGcKlXZuElZpFVMWtkSp9UaBhVZwo0QMl2Yq1UevtWVyEFbUNTWqlkNJNkWwRXbJNXSp5UMJpXVGpUaPl2YHJGaKlXZ')]][0] for __g['aap'] in [(__mod.b64decode)]][0])(__import__('base64', __g, __g, ('b64decode',), 0)) for __g['AV'] in [((lambda b, d: d.get('__metaclass__', getattr(b[0], '__class__', type(b[0])))('AV', b, d))((str,), (lambda __l: [__l for __l['__repr__'], __l['__repr__'].__name__ in [(lambda self: (lambda __l: [__name__ for __l['self'] in [(self)]][0])({}), '__repr__')]][0])({'__module__': __name__})))]][0])(globals()) +(lambda __g: [(lambda __mod: [[[None for __g['app_var'], app_var.__name__ in [(lambda index: (lambda __l: [[AV(aap(__l['var'].encode()).decode()) for __l['var'] in [(vars.split('acb2')[__l['index']][::(-1)])]][0] for __l['index'] in [(index)]][0])({}), 'app_var')]][0] for __g['vars'] in [('3YTNyUDOyQTOacb2=EmN5M2YjdzMhljYzYzYhlDMmFGNlVTOmNDZwMzNxYzNacb2=UDMzEGOyADO1QWO5kDNygTMlJGN5QzNzIWOmZTOiVmMacb2yMTNzITNacb2=UDZhJmMldTZ3QTY4IjZ3kTNxYjN0czNwI2YxkTM5MjNacb2==QMh5WOmZnewM2d4UDblRzZacb20QzMwAjNacb2=QzNiRTO3EjMjFzMldjY3QTMwEDMwADMiNWZ5UWO3UWMacb2RJ1UJpXUPlzdvZUZ0w2VzVjMq1mblRnZvBHZ4x2RMZWbqhVS5JkdQ1WS38FVDpFTw9WcthGb41GaoV3dQV1QHNVRutUMjRFe09VeGh1RO1yQFtkZ3RnL5IERNJTVE5EerpWTzUkaPlWUYlFcKNET3FkaOlXSU9EMRpnT49maJdHaYpVa3lWS0MXVUpXSU5URWRlT1kUaPlWTzMGcKlXZuElZpFVMWtkSp9UaBhVZwo0QMl2Yq1UevtWVyEFbUNTWqlkNJNkWwRXbJNXSp5UMJpXVGpUaPl2YHJGaKlXZ')]][0] for __g['aap'] in [(__mod.b64decode)]][0])(__import__('base64', __g, __g, ('b64decode',), 0)) for __g['AV'] in [((lambda b, d: d.get('__metaclass__', getattr(b[0], '__class__', type(b[0])))('AV', b, d))((str,), (lambda __l: [__l for __l['__repr__'], __l['__repr__'].__name__ in [(lambda self: (lambda __l: [__name__ for __l['self'] in [(self)]][0])({}), '__repr__')]][0])({'__module__': __name__})))]][0])(globals()) # type: ignore diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/helpers/audio.py similarity index 98% rename from music_assistant/server/helpers/audio.py rename to music_assistant/helpers/audio.py index 22dc1ef61..578bccf6d 100644 --- a/music_assistant/server/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -14,20 +14,16 @@ import aiofiles from aiohttp import ClientTimeout - -from music_assistant.common.helpers.global_cache import set_global_cache_values -from music_assistant.common.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads -from music_assistant.common.helpers.util import clean_stream_title -from music_assistant.common.models.config_entries import CoreConfig, PlayerConfig -from music_assistant.common.models.enums import MediaType, StreamType, VolumeNormalizationMode -from music_assistant.common.models.errors import ( +from music_assistant_models.enums import MediaType, StreamType, VolumeNormalizationMode +from music_assistant_models.errors import ( InvalidDataError, MediaNotFoundError, MusicAssistantError, ProviderUnavailableError, ) -from music_assistant.common.models.media_items import AudioFormat, ContentType -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.helpers.global_cache import set_global_cache_values +from music_assistant_models.media_items import AudioFormat, ContentType + from music_assistant.constants import ( CONF_EQ_BASS, CONF_EQ_MID, @@ -40,6 +36,8 @@ MASS_LOGGER_NAME, VERBOSE_LOG_LEVEL, ) +from music_assistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads +from music_assistant.helpers.util import clean_stream_title from .ffmpeg import FFMpeg, get_ffmpeg_stream from .playlists import IsHLSPlaylist, PlaylistItem, fetch_playlist, parse_m3u @@ -48,8 +46,11 @@ from .util import TimedAsyncGenerator, create_tempfile, detect_charset if TYPE_CHECKING: - from music_assistant.common.models.player_queue import QueueItem - from music_assistant.server import MusicAssistant + from music_assistant_models.config_entries import CoreConfig, PlayerConfig + from music_assistant_models.player_queue import QueueItem + from music_assistant_models.streamdetails import StreamDetails + + from music_assistant import MusicAssistant LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.audio") diff --git a/music_assistant/server/helpers/auth.py b/music_assistant/helpers/auth.py similarity index 94% rename from music_assistant/server/helpers/auth.py rename to music_assistant/helpers/auth.py index 76c47741d..04ae1947a 100644 --- a/music_assistant/server/helpers/auth.py +++ b/music_assistant/helpers/auth.py @@ -7,12 +7,11 @@ from typing import TYPE_CHECKING from aiohttp.web import Request, Response - -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.errors import LoginFailed +from music_assistant_models.enums import EventType +from music_assistant_models.errors import LoginFailed if TYPE_CHECKING: - from music_assistant.server import MusicAssistant + from music_assistant import MusicAssistant class AuthenticationHelper: diff --git a/music_assistant/server/helpers/compare.py b/music_assistant/helpers/compare.py similarity index 99% rename from music_assistant/server/helpers/compare.py rename to music_assistant/helpers/compare.py index 808b319f3..1d10fecb8 100644 --- a/music_assistant/server/helpers/compare.py +++ b/music_assistant/helpers/compare.py @@ -6,9 +6,8 @@ from difflib import SequenceMatcher import unidecode - -from music_assistant.common.models.enums import ExternalID, MediaType -from music_assistant.common.models.media_items import ( +from music_assistant_models.enums import ExternalID, MediaType +from music_assistant_models.media_items import ( Album, Artist, ItemMapping, diff --git a/music_assistant/server/helpers/database.py b/music_assistant/helpers/database.py similarity index 100% rename from music_assistant/server/helpers/database.py rename to music_assistant/helpers/database.py diff --git a/music_assistant/common/helpers/datetime.py b/music_assistant/helpers/datetime.py similarity index 100% rename from music_assistant/common/helpers/datetime.py rename to music_assistant/helpers/datetime.py diff --git a/music_assistant/server/helpers/didl_lite.py b/music_assistant/helpers/didl_lite.py similarity index 96% rename from music_assistant/server/helpers/didl_lite.py rename to music_assistant/helpers/didl_lite.py index 59b02c657..78ce38c7e 100644 --- a/music_assistant/server/helpers/didl_lite.py +++ b/music_assistant/helpers/didl_lite.py @@ -5,11 +5,12 @@ import datetime from typing import TYPE_CHECKING -from music_assistant.common.models.enums import MediaType +from music_assistant_models.enums import MediaType + from music_assistant.constants import MASS_LOGO_ONLINE if TYPE_CHECKING: - from music_assistant.common.models.player import PlayerMedia + from music_assistant_models.player import PlayerMedia # ruff: noqa: E501 diff --git a/music_assistant/server/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py similarity index 98% rename from music_assistant/server/helpers/ffmpeg.py rename to music_assistant/helpers/ffmpeg.py index 90e17ad8f..1cf911584 100644 --- a/music_assistant/server/helpers/ffmpeg.py +++ b/music_assistant/helpers/ffmpeg.py @@ -8,9 +8,10 @@ from collections.abc import AsyncGenerator from typing import TYPE_CHECKING -from music_assistant.common.helpers.global_cache import get_global_cache_value -from music_assistant.common.models.errors import AudioError -from music_assistant.common.models.media_items import AudioFormat, ContentType +from music_assistant_models.errors import AudioError +from music_assistant_models.helpers.global_cache import get_global_cache_value +from music_assistant_models.media_items import AudioFormat, ContentType + from music_assistant.constants import VERBOSE_LOG_LEVEL from .process import AsyncProcess diff --git a/music_assistant/common/helpers/global_cache.py b/music_assistant/helpers/global_cache.py similarity index 100% rename from music_assistant/common/helpers/global_cache.py rename to music_assistant/helpers/global_cache.py diff --git a/music_assistant/server/helpers/images.py b/music_assistant/helpers/images.py similarity index 92% rename from music_assistant/server/helpers/images.py rename to music_assistant/helpers/images.py index 931c63727..99e34c406 100644 --- a/music_assistant/server/helpers/images.py +++ b/music_assistant/helpers/images.py @@ -15,13 +15,14 @@ from aiohttp.client_exceptions import ClientError from PIL import Image, UnidentifiedImageError -from music_assistant.server.helpers.tags import get_embedded_image -from music_assistant.server.models.metadata_provider import MetadataProvider +from music_assistant.helpers.tags import get_embedded_image +from music_assistant.models.metadata_provider import MetadataProvider if TYPE_CHECKING: - from music_assistant.common.models.media_items import MediaItemImage - from music_assistant.server import MusicAssistant - from music_assistant.server.models.music_provider import MusicProvider + from music_assistant_models.media_items import MediaItemImage + + from music_assistant import MusicAssistant + from music_assistant.models.music_provider import MusicProvider async def get_image_data(mass: MusicAssistant, path_or_url: str, provider: str) -> bytes: diff --git a/music_assistant/common/helpers/json.py b/music_assistant/helpers/json.py similarity index 100% rename from music_assistant/common/helpers/json.py rename to music_assistant/helpers/json.py diff --git a/music_assistant/server/helpers/logging.py b/music_assistant/helpers/logging.py similarity index 100% rename from music_assistant/server/helpers/logging.py rename to music_assistant/helpers/logging.py diff --git a/music_assistant/server/helpers/playlists.py b/music_assistant/helpers/playlists.py similarity index 97% rename from music_assistant/server/helpers/playlists.py rename to music_assistant/helpers/playlists.py index 60f697034..9d72381b6 100644 --- a/music_assistant/server/helpers/playlists.py +++ b/music_assistant/helpers/playlists.py @@ -9,12 +9,12 @@ from urllib.parse import urlparse from aiohttp import client_exceptions +from music_assistant_models.errors import InvalidDataError -from music_assistant.common.models.errors import InvalidDataError -from music_assistant.server.helpers.util import detect_charset +from music_assistant.helpers.util import detect_charset if TYPE_CHECKING: - from music_assistant.server import MusicAssistant + from music_assistant import MusicAssistant LOGGER = logging.getLogger(__name__) diff --git a/music_assistant/server/helpers/process.py b/music_assistant/helpers/process.py similarity index 100% rename from music_assistant/server/helpers/process.py rename to music_assistant/helpers/process.py diff --git a/music_assistant/server/helpers/resources/announce.mp3 b/music_assistant/helpers/resources/announce.mp3 similarity index 100% rename from music_assistant/server/helpers/resources/announce.mp3 rename to music_assistant/helpers/resources/announce.mp3 diff --git a/music_assistant/server/helpers/resources/fallback_fanart.jpeg b/music_assistant/helpers/resources/fallback_fanart.jpeg similarity index 100% rename from music_assistant/server/helpers/resources/fallback_fanart.jpeg rename to music_assistant/helpers/resources/fallback_fanart.jpeg diff --git a/music_assistant/server/helpers/resources/logo.png b/music_assistant/helpers/resources/logo.png similarity index 100% rename from music_assistant/server/helpers/resources/logo.png rename to music_assistant/helpers/resources/logo.png diff --git a/music_assistant/server/helpers/resources/silence.mp3 b/music_assistant/helpers/resources/silence.mp3 similarity index 100% rename from music_assistant/server/helpers/resources/silence.mp3 rename to music_assistant/helpers/resources/silence.mp3 diff --git a/music_assistant/server/helpers/tags.py b/music_assistant/helpers/tags.py similarity index 98% rename from music_assistant/server/helpers/tags.py rename to music_assistant/helpers/tags.py index ba40710a6..818c2d310 100644 --- a/music_assistant/server/helpers/tags.py +++ b/music_assistant/helpers/tags.py @@ -12,13 +12,13 @@ from typing import Any import eyed3 +from music_assistant_models.enums import AlbumType +from music_assistant_models.errors import InvalidDataError +from music_assistant_models.media_items import MediaItemChapter -from music_assistant.common.helpers.util import try_parse_int -from music_assistant.common.models.enums import AlbumType -from music_assistant.common.models.errors import InvalidDataError -from music_assistant.common.models.media_items import MediaItemChapter from music_assistant.constants import MASS_LOGGER_NAME, UNKNOWN_ARTIST -from music_assistant.server.helpers.process import AsyncProcess +from music_assistant.helpers.process import AsyncProcess +from music_assistant.helpers.util import try_parse_int LOGGER = logging.getLogger(f"{MASS_LOGGER_NAME}.tags") diff --git a/music_assistant/server/helpers/throttle_retry.py b/music_assistant/helpers/throttle_retry.py similarity index 96% rename from music_assistant/server/helpers/throttle_retry.py rename to music_assistant/helpers/throttle_retry.py index 59bd9fb0b..74a957387 100644 --- a/music_assistant/server/helpers/throttle_retry.py +++ b/music_assistant/helpers/throttle_retry.py @@ -10,11 +10,12 @@ from contextvars import ContextVar from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar -from music_assistant.common.models.errors import ResourceTemporarilyUnavailable, RetriesExhausted +from music_assistant_models.errors import ResourceTemporarilyUnavailable, RetriesExhausted + from music_assistant.constants import MASS_LOGGER_NAME if TYPE_CHECKING: - from music_assistant.server.models.provider import Provider + from music_assistant.models.provider import Provider _ProviderT = TypeVar("_ProviderT", bound="Provider") _R = TypeVar("_R") diff --git a/music_assistant/common/helpers/uri.py b/music_assistant/helpers/uri.py similarity index 88% rename from music_assistant/common/helpers/uri.py rename to music_assistant/helpers/uri.py index a381861dc..7f39b4e1f 100644 --- a/music_assistant/common/helpers/uri.py +++ b/music_assistant/helpers/uri.py @@ -4,8 +4,8 @@ import os import re -from music_assistant.common.models.enums import MediaType -from music_assistant.common.models.errors import InvalidProviderID, InvalidProviderURI +from music_assistant_models.enums import MediaType +from music_assistant_models.errors import InvalidProviderID, InvalidProviderURI base62_length22_id_pattern = re.compile(r"^[a-zA-Z0-9]{22}$") @@ -72,8 +72,3 @@ async def parse_uri(uri: str, validate_id: bool = False) -> tuple[MediaType, str msg = f"Invalid {provider_instance_id_or_domain} ID: {item_id} found in URI: {uri}" raise InvalidProviderID(msg) return (media_type, provider_instance_id_or_domain, item_id) - - -def create_uri(media_type: MediaType, provider_instance_id_or_domain: str, item_id: str) -> str: - """Create Music Assistant URI from MediaItem values.""" - return f"{provider_instance_id_or_domain}://{media_type.value}/{item_id}" diff --git a/music_assistant/common/helpers/util.py b/music_assistant/helpers/util.py similarity index 51% rename from music_assistant/common/helpers/util.py rename to music_assistant/helpers/util.py index 7c14e89e9..25da351af 100644 --- a/music_assistant/common/helpers/util.py +++ b/music_assistant/helpers/util.py @@ -1,20 +1,52 @@ -"""Helper and utility functions.""" +"""Various (server-only) tools and helpers.""" from __future__ import annotations import asyncio +import functools +import importlib +import logging import os +import platform import re import socket -from collections.abc import Callable +import tempfile +import urllib.error +import urllib.parse +import urllib.request +from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine from collections.abc import Set as AbstractSet -from typing import Any, TypeVar +from contextlib import suppress +from functools import lru_cache +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as pkg_version +from types import TracebackType +from typing import TYPE_CHECKING, Any, ParamSpec, Self, TypeVar from urllib.parse import urlparse -from uuid import UUID + +import cchardet as chardet +import ifaddr +import memory_tempfile +from zeroconf import IPVersion + +from music_assistant.helpers.process import check_output + +if TYPE_CHECKING: + from collections.abc import Iterator + + from zeroconf.asyncio import AsyncServiceInfo + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderModuleType + +LOGGER = logging.getLogger(__name__) + +HA_WHEELS = "https://wheels.home-assistant.io/musllinux/" T = TypeVar("T") CALLBACK_TYPE = Callable[[], None] + keyword_pattern = re.compile("title=|artist=") title_pattern = re.compile(r"title=\"(?P.*?)\"") artist_pattern = re.compile(r"artist=\"(?P<artist>.*?)\"") @@ -94,15 +126,6 @@ def try_parse_duration(duration_str: str) -> float: return seconds + milliseconds -def create_sort_name(input_str: str) -> str: - """Create (basic/simple) sort name/title from string.""" - input_str = input_str.lower().strip() - for item in ["the ", "de ", "les ", "dj ", "las ", "los ", "le ", "la ", "el ", "a ", "an "]: - if input_str.startswith(item): - input_str = input_str.replace(item, "") + f", {item}" - return input_str.strip() - - def parse_title_and_version(title: str, track_version: str | None = None) -> tuple[str, str]: """Try to parse version from the title.""" version = track_version or "" @@ -275,33 +298,6 @@ def get_folder_size(folderpath: str) -> float: return total_size / float(1 << 30) -def merge_dict( - base_dict: dict[Any, Any], new_dict: dict[Any, Any], allow_overwite: bool = False -) -> dict[Any, Any]: - """Merge dict without overwriting existing values.""" - final_dict = base_dict.copy() - for key, value in new_dict.items(): - if final_dict.get(key) and isinstance(value, dict): - final_dict[key] = merge_dict(final_dict[key], value) - if final_dict.get(key) and isinstance(value, tuple): - final_dict[key] = merge_tuples(final_dict[key], value) - if final_dict.get(key) and isinstance(value, list): - final_dict[key] = merge_lists(final_dict[key], value) - elif not final_dict.get(key) or allow_overwite: - final_dict[key] = value - return final_dict - - -def merge_tuples(base: tuple[Any, ...], new: tuple[Any, ...]) -> tuple[Any, ...]: - """Merge 2 tuples.""" - return tuple(x for x in base if x not in new) + tuple(new) - - -def merge_lists(base: list[Any], new: list[Any]) -> list[Any]: - """Merge 2 lists.""" - return [x for x in base if x not in new] + list(new) - - def get_changed_keys( dict1: dict[str, Any], dict2: dict[str, Any], @@ -350,10 +346,288 @@ def empty_queue(q: asyncio.Queue[T]) -> None: pass -def is_valid_uuid(uuid_to_test: str) -> bool: - """Check if uuid string is a valid UUID.""" +async def install_package(package: str) -> None: + """Install package with pip, raise when install failed.""" + LOGGER.debug("Installing python package %s", package) + args = ["uv", "pip", "install", "--no-cache", "--find-links", HA_WHEELS, package] + return_code, output = await check_output(*args) + + if return_code != 0 and "Permission denied" in output.decode(): + # try again with regular pip + # uv pip seems to have issues with permissions on docker installs + args = [ + "pip", + "install", + "--no-cache-dir", + "--no-input", + "--find-links", + HA_WHEELS, + package, + ] + return_code, output = await check_output(*args) + + if return_code != 0: + msg = f"Failed to install package {package}\n{output.decode()}" + raise RuntimeError(msg) + + +async def get_package_version(pkg_name: str) -> str | None: + """ + Return the version of an installed (python) package. + + Will return None if the package is not found. + """ try: - uuid_obj = UUID(uuid_to_test) - except (ValueError, TypeError): + return await asyncio.to_thread(pkg_version, pkg_name) + except PackageNotFoundError: + return None + + +async def get_ips(include_ipv6: bool = False, ignore_loopback: bool = True) -> set[str]: + """Return all IP-adresses of all network interfaces.""" + + def call() -> set[str]: + result: set[str] = set() + adapters = ifaddr.get_adapters() + for adapter in adapters: + for ip in adapter.ips: + if ip.is_IPv6 and not include_ipv6: + continue + if ip.ip == "127.0.0.1" and ignore_loopback: + continue + result.add(ip.ip) + return result + + return await asyncio.to_thread(call) + + +async def is_hass_supervisor() -> bool: + """Return if we're running inside the HA Supervisor (e.g. HAOS).""" + + def _check(): + try: + urllib.request.urlopen("http://supervisor/core", timeout=1) + except urllib.error.URLError as err: + # this should return a 401 unauthorized if it exists + return getattr(err, "code", 999) == 401 + except Exception: + return False return False - return str(uuid_obj) == uuid_to_test + + return await asyncio.to_thread(_check) + + +async def load_provider_module(domain: str, requirements: list[str]) -> ProviderModuleType: + """Return module for given provider domain and make sure the requirements are met.""" + + @lru_cache + def _get_provider_module(domain: str) -> ProviderModuleType: + return importlib.import_module(f".{domain}", ".providers") + + # ensure module requirements are met + for requirement in requirements: + if "==" not in requirement: + # we should really get rid of unpinned requirements + continue + package_name, version = requirement.split("==", 1) + installed_version = await get_package_version(package_name) + if installed_version == "0.0.0": + # ignore editable installs + continue + if installed_version != version: + await install_package(requirement) + + # try to load the module + try: + return await asyncio.to_thread(_get_provider_module, domain) + except ImportError: + # (re)install ALL requirements + for requirement in requirements: + await install_package(requirement) + # try loading the provider again to be safe + # this will fail if something else is wrong (as it should) + return await asyncio.to_thread(_get_provider_module, domain) + + +def create_tempfile(): + """Return a (named) temporary file.""" + # ruff: noqa: SIM115 + if platform.system() == "Linux": + return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) + return tempfile.NamedTemporaryFile(buffering=0) + + +def divide_chunks(data: bytes, chunk_size: int) -> Iterator[bytes]: + """Chunk bytes data into smaller chunks.""" + for i in range(0, len(data), chunk_size): + yield data[i : i + chunk_size] + + +def get_primary_ip_address_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: + """Get primary IP address from zeroconf discovery info.""" + for address in discovery_info.parsed_addresses(IPVersion.V4Only): + if address.startswith("127"): + # filter out loopback address + continue + if address.startswith("169.254"): + # filter out APIPA address + continue + return address + return None + + +def get_port_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: + """Get primary IP address from zeroconf discovery info.""" + return discovery_info.port + + +async def close_async_generator(agen: AsyncGenerator[Any, None]) -> None: + """Force close an async generator.""" + task = asyncio.create_task(agen.__anext__()) + task.cancel() + with suppress(asyncio.CancelledError): + await task + await agen.aclose() + + +async def detect_charset(data: bytes, fallback="utf-8") -> str: + """Detect charset of raw data.""" + try: + detected = await asyncio.to_thread(chardet.detect, data) + if detected and detected["encoding"] and detected["confidence"] > 0.75: + return detected["encoding"] + except Exception as err: + LOGGER.debug("Failed to detect charset: %s", err) + return fallback + + +def merge_dict( + base_dict: dict[Any, Any], + new_dict: dict[Any, Any], + allow_overwite: bool = False, +) -> dict[Any, Any]: + """Merge dict without overwriting existing values.""" + final_dict = base_dict.copy() + for key, value in new_dict.items(): + if final_dict.get(key) and isinstance(value, dict): + final_dict[key] = merge_dict(final_dict[key], value) + if final_dict.get(key) and isinstance(value, tuple): + final_dict[key] = merge_tuples(final_dict[key], value) + if final_dict.get(key) and isinstance(value, list): + final_dict[key] = merge_lists(final_dict[key], value) + elif not final_dict.get(key) or allow_overwite: + final_dict[key] = value + return final_dict + + +def merge_tuples(base: tuple[Any, ...], new: tuple[Any, ...]) -> tuple[Any, ...]: + """Merge 2 tuples.""" + return tuple(x for x in base if x not in new) + tuple(new) + + +def merge_lists(base: list[Any], new: list[Any]) -> list[Any]: + """Merge 2 lists.""" + return [x for x in base if x not in new] + list(new) + + +class TaskManager: + """ + Helper class to run many tasks at once. + + This is basically an alternative to asyncio.TaskGroup but this will not + cancel all operations when one of the tasks fails. + Logging of exceptions is done by the mass.create_task helper. + """ + + def __init__(self, mass: MusicAssistant, limit: int = 0): + """Initialize the TaskManager.""" + self.mass = mass + self._tasks: list[asyncio.Task] = [] + self._semaphore = asyncio.Semaphore(limit) if limit else None + + def create_task(self, coro: Coroutine) -> asyncio.Task: + """Create a new task and add it to the manager.""" + task = self.mass.create_task(coro) + self._tasks.append(task) + return task + + async def create_task_with_limit(self, coro: Coroutine) -> None: + """Create a new task with semaphore limit.""" + assert self._semaphore is not None + + def task_done_callback(_task: asyncio.Task) -> None: + self._tasks.remove(task) + self._semaphore.release() + + await self._semaphore.acquire() + task: asyncio.Task = self.create_task(coro) + task.add_done_callback(task_done_callback) + + async def __aenter__(self) -> Self: + """Enter context manager.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit context manager.""" + if len(self._tasks) > 0: + await asyncio.wait(self._tasks) + self._tasks.clear() + + +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def lock( + func: Callable[_P, Awaitable[_R]], +) -> Callable[_P, Coroutine[Any, Any, _R]]: + """Call async function using a Lock.""" + + @functools.wraps(func) + async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: + """Call async function using the throttler with retries.""" + if not (func_lock := getattr(func, "lock", None)): + func_lock = asyncio.Lock() + func.lock = func_lock + async with func_lock: + return await func(*args, **kwargs) + + return wrapper + + +class TimedAsyncGenerator: + """ + Async iterable that times out after a given time. + + Source: https://medium.com/@dmitry8912/implementing-timeouts-in-pythons-asynchronous-generators-f7cbaa6dc1e9 + """ + + def __init__(self, iterable, timeout=0): + """ + Initialize the AsyncTimedIterable. + + Args: + iterable: The async iterable to wrap. + timeout: The timeout in seconds for each iteration. + """ + + class AsyncTimedIterator: + def __init__(self): + self._iterator = iterable.__aiter__() + + async def __anext__(self): + result = await asyncio.wait_for(self._iterator.__anext__(), int(timeout)) + if not result: + raise StopAsyncIteration + return result + + self._factory = AsyncTimedIterator + + def __aiter__(self): + """Return the async iterator.""" + return self._factory() diff --git a/music_assistant/server/helpers/webserver.py b/music_assistant/helpers/webserver.py similarity index 100% rename from music_assistant/server/helpers/webserver.py rename to music_assistant/helpers/webserver.py diff --git a/music_assistant/server/server.py b/music_assistant/mass.py similarity index 95% rename from music_assistant/server/server.py rename to music_assistant/mass.py index 48e9e8801..bb4119da5 100644 --- a/music_assistant/server/server.py +++ b/music_assistant/mass.py @@ -12,16 +12,15 @@ import aiofiles from aiofiles.os import wrap from aiohttp import ClientSession, TCPConnector +from music_assistant_models.api import ServerInfoMessage +from music_assistant_models.enums import EventType, ProviderType +from music_assistant_models.errors import MusicAssistantError, SetupFailedError +from music_assistant_models.event import MassEvent +from music_assistant_models.helpers.global_cache import set_global_cache_values +from music_assistant_models.provider import ProviderManifest from zeroconf import IPVersion, NonUniqueNameException, ServiceStateChange, Zeroconf from zeroconf.asyncio import AsyncServiceBrowser, AsyncServiceInfo, AsyncZeroconf -from music_assistant.common.helpers.global_cache import set_global_cache_values -from music_assistant.common.helpers.util import get_ip_pton -from music_assistant.common.models.api import ServerInfoMessage -from music_assistant.common.models.enums import EventType, ProviderType -from music_assistant.common.models.errors import MusicAssistantError, SetupFailedError -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.provider import ProviderManifest from music_assistant.constants import ( API_SCHEMA_VERSION, CONF_PROVIDERS, @@ -31,30 +30,31 @@ MIN_SCHEMA_VERSION, VERBOSE_LOG_LEVEL, ) -from music_assistant.server.controllers.cache import CacheController -from music_assistant.server.controllers.config import ConfigController -from music_assistant.server.controllers.metadata import MetaDataController -from music_assistant.server.controllers.music import MusicController -from music_assistant.server.controllers.player_queues import PlayerQueuesController -from music_assistant.server.controllers.players import PlayerController -from music_assistant.server.controllers.streams import StreamsController -from music_assistant.server.controllers.webserver import WebserverController -from music_assistant.server.helpers.api import APICommandHandler, api_command -from music_assistant.server.helpers.images import get_icon_string -from music_assistant.server.helpers.util import ( +from music_assistant.controllers.cache import CacheController +from music_assistant.controllers.config import ConfigController +from music_assistant.controllers.metadata import MetaDataController +from music_assistant.controllers.music import MusicController +from music_assistant.controllers.player_queues import PlayerQueuesController +from music_assistant.controllers.players import PlayerController +from music_assistant.controllers.streams import StreamsController +from music_assistant.controllers.webserver import WebserverController +from music_assistant.helpers.api import APICommandHandler, api_command +from music_assistant.helpers.images import get_icon_string +from music_assistant.helpers.util import ( TaskManager, + get_ip_pton, get_package_version, is_hass_supervisor, load_provider_module, ) - -from .models import ProviderInstanceType +from music_assistant.models import ProviderInstanceType if TYPE_CHECKING: from types import TracebackType - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.server.models.core_controller import CoreController + from music_assistant_models.config_entries import ProviderConfig + + from music_assistant.models.core_controller import CoreController isdir = wrap(os.path.isdir) isfile = wrap(os.path.isfile) diff --git a/music_assistant/server/models/__init__.py b/music_assistant/models/__init__.py similarity index 82% rename from music_assistant/server/models/__init__.py rename to music_assistant/models/__init__.py index 4e20ea729..ce2bab180 100644 --- a/music_assistant/server/models/__init__.py +++ b/music_assistant/models/__init__.py @@ -10,13 +10,10 @@ from .plugin import PluginProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, - ) - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant ProviderInstanceType = MetadataProvider | MusicProvider | PlayerProvider | PluginProvider diff --git a/music_assistant/server/models/core_controller.py b/music_assistant/models/core_controller.py similarity index 88% rename from music_assistant/server/models/core_controller.py rename to music_assistant/models/core_controller.py index 3965b89af..7f2532c32 100644 --- a/music_assistant/server/models/core_controller.py +++ b/music_assistant/models/core_controller.py @@ -5,17 +5,15 @@ import logging from typing import TYPE_CHECKING -from music_assistant.common.models.enums import ProviderType -from music_assistant.common.models.provider import ProviderManifest +from music_assistant_models.enums import ProviderType +from music_assistant_models.provider import ProviderManifest + from music_assistant.constants import CONF_LOG_LEVEL, MASS_LOGGER_NAME if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - CoreConfig, - ) - from music_assistant.server import MusicAssistant + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, CoreConfig + + from music_assistant import MusicAssistant class CoreController: diff --git a/music_assistant/server/models/metadata_provider.py b/music_assistant/models/metadata_provider.py similarity index 91% rename from music_assistant/server/models/metadata_provider.py rename to music_assistant/models/metadata_provider.py index f19432feb..25f28aa70 100644 --- a/music_assistant/server/models/metadata_provider.py +++ b/music_assistant/models/metadata_provider.py @@ -4,12 +4,12 @@ from typing import TYPE_CHECKING -from music_assistant.common.models.enums import ProviderFeature +from music_assistant_models.enums import ProviderFeature from .provider import Provider if TYPE_CHECKING: - from music_assistant.common.models.media_items import Album, Artist, MediaItemMetadata, Track + from music_assistant_models.media_items import Album, Artist, MediaItemMetadata, Track # ruff: noqa: ARG001, ARG002 diff --git a/music_assistant/server/models/music_provider.py b/music_assistant/models/music_provider.py similarity index 98% rename from music_assistant/server/models/music_provider.py rename to music_assistant/models/music_provider.py index 79e49e935..5fa57be51 100644 --- a/music_assistant/server/models/music_provider.py +++ b/music_assistant/models/music_provider.py @@ -6,9 +6,9 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, cast -from music_assistant.common.models.enums import CacheCategory, MediaType, ProviderFeature -from music_assistant.common.models.errors import MediaNotFoundError, MusicAssistantError -from music_assistant.common.models.media_items import ( +from music_assistant_models.enums import CacheCategory, MediaType, ProviderFeature +from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError +from music_assistant_models.media_items import ( Album, Artist, BrowseFolder, @@ -19,13 +19,14 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails from .provider import Provider if TYPE_CHECKING: from collections.abc import AsyncGenerator + from music_assistant_models.streamdetails import StreamDetails + # ruff: noqa: ARG001, ARG002 diff --git a/music_assistant/server/models/player_provider.py b/music_assistant/models/player_provider.py similarity index 97% rename from music_assistant/server/models/player_provider.py rename to music_assistant/models/player_provider.py index 51ba82291..8d54db53d 100644 --- a/music_assistant/server/models/player_provider.py +++ b/music_assistant/models/player_provider.py @@ -3,11 +3,9 @@ from __future__ import annotations from abc import abstractmethod +from typing import TYPE_CHECKING -from zeroconf import ServiceStateChange -from zeroconf.asyncio import AsyncServiceInfo - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( BASE_PLAYER_CONFIG_ENTRIES, CONF_ENTRY_ANNOUNCE_VOLUME, CONF_ENTRY_ANNOUNCE_VOLUME_MAX, @@ -16,11 +14,15 @@ ConfigEntry, PlayerConfig, ) -from music_assistant.common.models.errors import UnsupportedFeaturedException -from music_assistant.common.models.player import Player, PlayerMedia +from music_assistant_models.errors import UnsupportedFeaturedException +from zeroconf import ServiceStateChange +from zeroconf.asyncio import AsyncServiceInfo from .provider import Provider +if TYPE_CHECKING: + from music_assistant_models.player import Player, PlayerMedia + # ruff: noqa: ARG001, ARG002 diff --git a/music_assistant/server/models/plugin.py b/music_assistant/models/plugin.py similarity index 100% rename from music_assistant/server/models/plugin.py rename to music_assistant/models/plugin.py diff --git a/music_assistant/server/models/provider.py b/music_assistant/models/provider.py similarity index 92% rename from music_assistant/server/models/provider.py rename to music_assistant/models/provider.py index dc415a4ce..89b133799 100644 --- a/music_assistant/server/models/provider.py +++ b/music_assistant/models/provider.py @@ -8,13 +8,13 @@ from music_assistant.constants import CONF_LOG_LEVEL, MASS_LOGGER_NAME if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.enums import ProviderFeature, ProviderType + from music_assistant_models.provider import ProviderManifest from zeroconf import ServiceStateChange from zeroconf.asyncio import AsyncServiceInfo - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.enums import ProviderFeature, ProviderType - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant + from music_assistant import MusicAssistant class Provider: diff --git a/music_assistant/server/providers/__init__.py b/music_assistant/providers/__init__.py similarity index 100% rename from music_assistant/server/providers/__init__.py rename to music_assistant/providers/__init__.py diff --git a/music_assistant/server/providers/_template_music_provider/__init__.py b/music_assistant/providers/_template_music_provider/__init__.py similarity index 97% rename from music_assistant/server/providers/_template_music_provider/__init__.py rename to music_assistant/providers/_template_music_provider/__init__.py index ee8a0cee9..89cbba27d 100644 --- a/music_assistant/server/providers/_template_music_provider/__init__.py +++ b/music_assistant/providers/_template_music_provider/__init__.py @@ -40,9 +40,8 @@ from collections.abc import AsyncGenerator, Sequence from typing import TYPE_CHECKING -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ContentType, MediaType, ProviderFeature, StreamType -from music_assistant.common.models.media_items import ( +from music_assistant_models.enums import ContentType, MediaType, ProviderFeature, StreamType +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, @@ -54,14 +53,16 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType async def setup( diff --git a/music_assistant/server/providers/_template_music_provider/icon.svg b/music_assistant/providers/_template_music_provider/icon.svg similarity index 100% rename from music_assistant/server/providers/_template_music_provider/icon.svg rename to music_assistant/providers/_template_music_provider/icon.svg diff --git a/music_assistant/server/providers/_template_music_provider/manifest.json b/music_assistant/providers/_template_music_provider/manifest.json similarity index 100% rename from music_assistant/server/providers/_template_music_provider/manifest.json rename to music_assistant/providers/_template_music_provider/manifest.json diff --git a/music_assistant/server/providers/_template_player_provider/__init__.py b/music_assistant/providers/_template_player_provider/__init__.py similarity index 96% rename from music_assistant/server/providers/_template_player_provider/__init__.py rename to music_assistant/providers/_template_player_provider/__init__.py index cf2b897c0..ae3cd3090 100644 --- a/music_assistant/server/providers/_template_player_provider/__init__.py +++ b/music_assistant/providers/_template_player_provider/__init__.py @@ -33,21 +33,25 @@ from typing import TYPE_CHECKING +from music_assistant_models.enums import PlayerFeature, PlayerType, ProviderFeature +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia from zeroconf import ServiceStateChange -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType, PlayerConfig -from music_assistant.common.models.enums import PlayerFeature, PlayerType, ProviderFeature -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.helpers.util import get_primary_ip_address_from_zeroconf -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant.helpers.util import get_primary_ip_address_from_zeroconf +from music_assistant.models.player_provider import PlayerProvider if TYPE_CHECKING: + from music_assistant_models.config_entries import ( + ConfigEntry, + ConfigValueType, + PlayerConfig, + ProviderConfig, + ) + from music_assistant_models.provider import ProviderManifest from zeroconf.asyncio import AsyncServiceInfo - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType async def setup( diff --git a/music_assistant/server/providers/_template_player_provider/icon.svg b/music_assistant/providers/_template_player_provider/icon.svg similarity index 100% rename from music_assistant/server/providers/_template_player_provider/icon.svg rename to music_assistant/providers/_template_player_provider/icon.svg diff --git a/music_assistant/server/providers/_template_player_provider/manifest.json b/music_assistant/providers/_template_player_provider/manifest.json similarity index 100% rename from music_assistant/server/providers/_template_player_provider/manifest.json rename to music_assistant/providers/_template_player_provider/manifest.json diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/providers/airplay/__init__.py similarity index 69% rename from music_assistant/server/providers/airplay/__init__.py rename to music_assistant/providers/airplay/__init__.py index 1a29387ec..83081ea52 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/providers/airplay/__init__.py @@ -4,23 +4,21 @@ from typing import TYPE_CHECKING -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.server import MusicAssistant +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.provider import ProviderManifest + +from music_assistant import MusicAssistant from .const import CONF_BIND_INTERFACE from .provider import AirplayProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 async def get_config_entries( diff --git a/music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 b/music_assistant/providers/airplay/bin/cliraop-linux-aarch64 similarity index 100% rename from music_assistant/server/providers/airplay/bin/cliraop-linux-aarch64 rename to music_assistant/providers/airplay/bin/cliraop-linux-aarch64 diff --git a/music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 b/music_assistant/providers/airplay/bin/cliraop-linux-x86_64 similarity index 100% rename from music_assistant/server/providers/airplay/bin/cliraop-linux-x86_64 rename to music_assistant/providers/airplay/bin/cliraop-linux-x86_64 diff --git a/music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 b/music_assistant/providers/airplay/bin/cliraop-macos-arm64 similarity index 100% rename from music_assistant/server/providers/airplay/bin/cliraop-macos-arm64 rename to music_assistant/providers/airplay/bin/cliraop-macos-arm64 diff --git a/music_assistant/server/providers/airplay/const.py b/music_assistant/providers/airplay/const.py similarity index 85% rename from music_assistant/server/providers/airplay/const.py rename to music_assistant/providers/airplay/const.py index 3d9ecd8d0..ea106adc7 100644 --- a/music_assistant/server/providers/airplay/const.py +++ b/music_assistant/providers/airplay/const.py @@ -2,8 +2,8 @@ from __future__ import annotations -from music_assistant.common.models.enums import ContentType -from music_assistant.common.models.media_items import AudioFormat +from music_assistant_models.enums import ContentType +from music_assistant_models.media_items import AudioFormat DOMAIN = "airplay" diff --git a/music_assistant/server/providers/airplay/helpers.py b/music_assistant/providers/airplay/helpers.py similarity index 100% rename from music_assistant/server/providers/airplay/helpers.py rename to music_assistant/providers/airplay/helpers.py diff --git a/music_assistant/server/providers/airplay/manifest.json b/music_assistant/providers/airplay/manifest.json similarity index 100% rename from music_assistant/server/providers/airplay/manifest.json rename to music_assistant/providers/airplay/manifest.json diff --git a/music_assistant/server/providers/airplay/player.py b/music_assistant/providers/airplay/player.py similarity index 96% rename from music_assistant/server/providers/airplay/player.py rename to music_assistant/providers/airplay/player.py index db33b64f7..8eed6323e 100644 --- a/music_assistant/server/providers/airplay/player.py +++ b/music_assistant/providers/airplay/player.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING -from music_assistant.common.models.enums import PlayerState +from music_assistant_models.enums import PlayerState if TYPE_CHECKING: from zeroconf.asyncio import AsyncServiceInfo diff --git a/music_assistant/server/providers/airplay/provider.py b/music_assistant/providers/airplay/provider.py similarity index 96% rename from music_assistant/server/providers/airplay/provider.py rename to music_assistant/providers/airplay/provider.py index f7977cef7..34904afef 100644 --- a/music_assistant/server/providers/airplay/provider.py +++ b/music_assistant/providers/airplay/provider.py @@ -10,12 +10,7 @@ from random import randrange from typing import TYPE_CHECKING -from zeroconf import ServiceStateChange -from zeroconf.asyncio import AsyncServiceInfo - -from music_assistant.common.helpers.datetime import utc -from music_assistant.common.helpers.util import get_ip_pton, select_free_port -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_EQ_BASS, @@ -27,7 +22,7 @@ ConfigEntry, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ConfigEntryType, ContentType, MediaType, @@ -36,13 +31,22 @@ PlayerType, ProviderFeature, ) -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.helpers.audio import get_ffmpeg_stream -from music_assistant.server.helpers.process import check_output -from music_assistant.server.helpers.util import TaskManager, lock -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.airplay.raop import RaopStreamSession +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from zeroconf import ServiceStateChange +from zeroconf.asyncio import AsyncServiceInfo + +from music_assistant.helpers import ( + convert_airplay_volume, + get_model_from_am, + get_primary_ip_address, +) +from music_assistant.helpers.audio import get_ffmpeg_stream +from music_assistant.helpers.datetime import utc +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 .const import ( AIRPLAY_FLOW_PCM_FORMAT, @@ -54,11 +58,10 @@ CONF_READ_AHEAD_BUFFER, FALLBACK_VOLUME, ) -from .helpers import convert_airplay_volume, get_model_from_am, get_primary_ip_address from .player import AirPlayPlayer if TYPE_CHECKING: - from music_assistant.server.providers.player_group import PlayerGroupProvider + from music_assistant.providers.player_group import PlayerGroupProvider PLAYER_CONFIG_ENTRIES = ( diff --git a/music_assistant/server/providers/airplay/raop.py b/music_assistant/providers/airplay/raop.py similarity index 97% rename from music_assistant/server/providers/airplay/raop.py rename to music_assistant/providers/airplay/raop.py index 27f4d701d..e1b74942b 100644 --- a/music_assistant/server/providers/airplay/raop.py +++ b/music_assistant/providers/airplay/raop.py @@ -12,12 +12,13 @@ from random import randint from typing import TYPE_CHECKING -from music_assistant.common.models.enums import PlayerState +from music_assistant_models.enums import PlayerState + from music_assistant.constants import CONF_SYNC_ADJUST, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.audio import get_player_filter_params -from music_assistant.server.helpers.ffmpeg import FFMpeg -from music_assistant.server.helpers.process import AsyncProcess, check_output -from music_assistant.server.helpers.util import close_async_generator +from music_assistant.helpers.audio import get_player_filter_params +from music_assistant.helpers.ffmpeg import FFMpeg +from music_assistant.helpers.process import AsyncProcess, check_output +from music_assistant.helpers.util import close_async_generator from .const import ( AIRPLAY_PCM_FORMAT, @@ -29,8 +30,8 @@ ) if TYPE_CHECKING: - from music_assistant.common.models.media_items import AudioFormat - from music_assistant.common.models.player_queue import PlayerQueue + from music_assistant_models.media_items import AudioFormat + from music_assistant_models.player_queue import PlayerQueue from .player import AirPlayPlayer from .provider import AirplayProvider diff --git a/music_assistant/server/providers/apple_music/__init__.py b/music_assistant/providers/apple_music/__init__.py similarity index 97% rename from music_assistant/server/providers/apple_music/__init__.py rename to music_assistant/providers/apple_music/__init__.py index 03d69a540..e753449f7 100644 --- a/music_assistant/server/providers/apple_music/__init__.py +++ b/music_assistant/providers/apple_music/__init__.py @@ -8,19 +8,10 @@ from typing import TYPE_CHECKING, Any import aiofiles -from pywidevine import PSSH, Cdm, Device, DeviceTypes -from pywidevine.license_protocol_pb2 import WidevinePsshData - -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable -from music_assistant.common.models.media_items import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -36,20 +27,25 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails +from pywidevine import PSSH, Cdm, Device, DeviceTypes +from pywidevine.license_protocol_pb2 import WidevinePsshData + from music_assistant.constants import CONF_PASSWORD -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.playlists import fetch_playlist -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.playlists import fetch_playlist +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: from collections.abc import AsyncGenerator - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 SUPPORTED_FEATURES = ( diff --git a/music_assistant/server/providers/apple_music/bin/README.md b/music_assistant/providers/apple_music/bin/README.md similarity index 100% rename from music_assistant/server/providers/apple_music/bin/README.md rename to music_assistant/providers/apple_music/bin/README.md diff --git a/music_assistant/server/providers/apple_music/icon.svg b/music_assistant/providers/apple_music/icon.svg similarity index 100% rename from music_assistant/server/providers/apple_music/icon.svg rename to music_assistant/providers/apple_music/icon.svg diff --git a/music_assistant/server/providers/apple_music/manifest.json b/music_assistant/providers/apple_music/manifest.json similarity index 100% rename from music_assistant/server/providers/apple_music/manifest.json rename to music_assistant/providers/apple_music/manifest.json diff --git a/music_assistant/server/providers/bluesound/__init__.py b/music_assistant/providers/bluesound/__init__.py similarity index 95% rename from music_assistant/server/providers/bluesound/__init__.py rename to music_assistant/providers/bluesound/__init__.py index 3a7b39010..bc718b4a2 100644 --- a/music_assistant/server/providers/bluesound/__init__.py +++ b/music_assistant/providers/bluesound/__init__.py @@ -6,11 +6,7 @@ import time from typing import TYPE_CHECKING, TypedDict -from pyblu import Player as BluosPlayer -from pyblu import Status, SyncStatus -from zeroconf import ServiceStateChange - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_ENABLE_ICY_METADATA, CONF_ENTRY_ENFORCE_MP3, @@ -19,28 +15,27 @@ ConfigEntry, ConfigValueType, ) -from music_assistant.common.models.enums import ( - PlayerFeature, - PlayerState, - PlayerType, - ProviderFeature, -) -from music_assistant.common.models.errors import PlayerCommandFailed -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant_models.enums import PlayerFeature, PlayerState, PlayerType, ProviderFeature +from music_assistant_models.errors import PlayerCommandFailed +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from pyblu import Player as BluosPlayer +from pyblu import Status, SyncStatus +from zeroconf import ServiceStateChange + from music_assistant.constants import VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.util import ( +from music_assistant.helpers.util import ( get_port_from_zeroconf, get_primary_ip_address_from_zeroconf, ) -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant.models.player_provider import PlayerProvider if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest from zeroconf.asyncio import AsyncServiceInfo - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType PLAYER_FEATURES_BASE = { diff --git a/music_assistant/server/providers/bluesound/icon.svg b/music_assistant/providers/bluesound/icon.svg similarity index 100% rename from music_assistant/server/providers/bluesound/icon.svg rename to music_assistant/providers/bluesound/icon.svg diff --git a/music_assistant/server/providers/bluesound/manifest.json b/music_assistant/providers/bluesound/manifest.json similarity index 100% rename from music_assistant/server/providers/bluesound/manifest.json rename to music_assistant/providers/bluesound/manifest.json diff --git a/music_assistant/server/providers/builtin/__init__.py b/music_assistant/providers/builtin/__init__.py similarity index 96% rename from music_assistant/server/providers/builtin/__init__.py rename to music_assistant/providers/builtin/__init__.py index a5a4b0ed0..b9b302011 100644 --- a/music_assistant/server/providers/builtin/__init__.py +++ b/music_assistant/providers/builtin/__init__.py @@ -10,10 +10,8 @@ import aiofiles import shortuuid - -from music_assistant.common.helpers.uri import parse_uri -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ( CacheCategory, ConfigEntryType, ContentType, @@ -22,12 +20,13 @@ ProviderFeature, StreamType, ) -from music_assistant.common.models.errors import ( +from music_assistant_models.errors import ( InvalidDataError, MediaNotFoundError, ProviderUnavailableError, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.helpers.uri import parse_uri +from music_assistant_models.media_items import ( Artist, AudioFormat, MediaItemImage, @@ -39,16 +38,18 @@ Track, UniqueList, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import MASS_LOGO, RESOURCES_DIR, VARIOUS_ARTISTS_FANART -from music_assistant.server.helpers.tags import AudioTags, parse_tags -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.tags import AudioTags, parse_tags +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType class StoredItem(TypedDict): @@ -563,7 +564,7 @@ async def _get_builtin_playlist_random_tracks(self) -> list[Track]: result.append(item) return result - async def _get_builtin_playlist_random_album(self) -> list[Track]: + async def _get_builtin_playlist_random_album(self) -> UniqueList[Track]: for in_library_only in (True, False): for min_tracks_required in (10, 5, 1): for random_album in await self.mass.music.albums.library_items( @@ -577,9 +578,9 @@ async def _get_builtin_playlist_random_album(self) -> list[Track]: for idx, track in enumerate(tracks, 1): track.position = idx return tracks - return [] + return UniqueList() - async def _get_builtin_playlist_random_artist(self) -> list[Track]: + async def _get_builtin_playlist_random_artist(self) -> UniqueList[Track]: for in_library_only in (True, False): for min_tracks_required in (25, 10, 5, 1): for random_artist in await self.mass.music.artists.library_items( @@ -595,7 +596,7 @@ async def _get_builtin_playlist_random_artist(self) -> list[Track]: for idx, track in enumerate(tracks, 1): track.position = idx return tracks - return [] + return UniqueList() async def _get_builtin_playlist_recently_played(self) -> list[Track]: result: list[Track] = [] @@ -606,7 +607,9 @@ async def _get_builtin_playlist_recently_played(self) -> list[Track]: result.append(track) return result - async def _get_builtin_playlist_tracks(self, builtin_playlist_id: str) -> list[Track]: + async def _get_builtin_playlist_tracks( + self, builtin_playlist_id: str + ) -> list[Track] | UniqueList[Track]: """Get all playlist tracks for given builtin playlist id.""" try: return await { diff --git a/music_assistant/server/providers/builtin/icon.svg b/music_assistant/providers/builtin/icon.svg similarity index 100% rename from music_assistant/server/providers/builtin/icon.svg rename to music_assistant/providers/builtin/icon.svg diff --git a/music_assistant/server/providers/builtin/manifest.json b/music_assistant/providers/builtin/manifest.json similarity index 100% rename from music_assistant/server/providers/builtin/manifest.json rename to music_assistant/providers/builtin/manifest.json diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py similarity index 97% rename from music_assistant/server/providers/chromecast/__init__.py rename to music_assistant/providers/chromecast/__init__.py index 6d13fa429..58898ed8e 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -12,12 +12,7 @@ from uuid import UUID import pychromecast -from pychromecast.controllers.media import STREAM_TYPE_BUFFERED, STREAM_TYPE_LIVE, MediaController -from pychromecast.controllers.multizone import MultizoneController, MultizoneManager -from pychromecast.discovery import CastBrowser, SimpleCastListener -from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( BASE_PLAYER_CONFIG_ENTRIES, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, @@ -26,29 +21,33 @@ ConfigValueType, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import MediaType, PlayerFeature, PlayerState, PlayerType -from music_assistant.common.models.errors import PlayerUnavailableError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant_models.enums import MediaType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import PlayerUnavailableError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from pychromecast.controllers.media import STREAM_TYPE_BUFFERED, STREAM_TYPE_LIVE, MediaController +from pychromecast.controllers.multizone import MultizoneController, MultizoneManager +from pychromecast.discovery import CastBrowser, SimpleCastListener +from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED + from music_assistant.constants import ( CONF_ENFORCE_MP3, CONF_PLAYERS, MASS_LOGO_ONLINE, VERBOSE_LOG_LEVEL, ) -from music_assistant.server.models.player_provider import PlayerProvider - -from .helpers import CastStatusListener, ChromecastInfo +from music_assistant.helpers import CastStatusListener, ChromecastInfo +from music_assistant.models.player_provider import PlayerProvider if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest from pychromecast.controllers.media import MediaStatus from pychromecast.controllers.receiver import CastStatus from pychromecast.models import CastInfo from pychromecast.socket_client import ConnectionStatus - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType PLAYER_CONFIG_ENTRIES = ( diff --git a/music_assistant/server/providers/chromecast/helpers.py b/music_assistant/providers/chromecast/helpers.py similarity index 100% rename from music_assistant/server/providers/chromecast/helpers.py rename to music_assistant/providers/chromecast/helpers.py diff --git a/music_assistant/server/providers/chromecast/manifest.json b/music_assistant/providers/chromecast/manifest.json similarity index 100% rename from music_assistant/server/providers/chromecast/manifest.json rename to music_assistant/providers/chromecast/manifest.json diff --git a/music_assistant/server/providers/deezer/__init__.py b/music_assistant/providers/deezer/__init__.py similarity index 97% rename from music_assistant/server/providers/deezer/__init__.py rename to music_assistant/providers/deezer/__init__.py index 93c3614c8..eb79c4900 100644 --- a/music_assistant/server/providers/deezer/__init__.py +++ b/music_assistant/providers/deezer/__init__.py @@ -11,14 +11,8 @@ from aiohttp import ClientSession, ClientTimeout from Crypto.Cipher import Blowfish from deezer import exceptions as deezer_exceptions - -from music_assistant.common.helpers.datetime import utc_timestamp -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ( AlbumType, ConfigEntryType, ContentType, @@ -28,8 +22,8 @@ ProviderFeature, StreamType, ) -from music_assistant.common.models.errors import LoginFailed -from music_assistant.common.models.media_items import ( +from music_assistant_models.errors import LoginFailed +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, @@ -42,13 +36,15 @@ SearchResults, Track, ) -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.models import ProviderInstanceType -from music_assistant.server.models.music_provider import MusicProvider -from music_assistant.server.server import MusicAssistant +from music_assistant_models.provider import ProviderManifest +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant import MusicAssistant +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.datetime import utc_timestamp +from music_assistant.models import ProviderInstanceType +from music_assistant.models.music_provider import MusicProvider from .gw_client import GWClient diff --git a/music_assistant/server/providers/deezer/gw_client.py b/music_assistant/providers/deezer/gw_client.py similarity index 98% rename from music_assistant/server/providers/deezer/gw_client.py rename to music_assistant/providers/deezer/gw_client.py index 0a03c298f..e0e83f753 100644 --- a/music_assistant/server/providers/deezer/gw_client.py +++ b/music_assistant/providers/deezer/gw_client.py @@ -8,10 +8,10 @@ from http.cookies import BaseCookie, Morsel from aiohttp import ClientSession +from music_assistant_models.streamdetails import StreamDetails from yarl import URL -from music_assistant.common.helpers.datetime import utc_timestamp -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant.helpers.datetime import utc_timestamp USER_AGENT_HEADER = ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " diff --git a/music_assistant/server/providers/deezer/icon.svg b/music_assistant/providers/deezer/icon.svg similarity index 100% rename from music_assistant/server/providers/deezer/icon.svg rename to music_assistant/providers/deezer/icon.svg diff --git a/music_assistant/server/providers/deezer/manifest.json b/music_assistant/providers/deezer/manifest.json similarity index 100% rename from music_assistant/server/providers/deezer/manifest.json rename to music_assistant/providers/deezer/manifest.json diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/providers/dlna/__init__.py similarity index 96% rename from music_assistant/server/providers/dlna/__init__.py rename to music_assistant/providers/dlna/__init__.py index fadc3dff4..97857d013 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/providers/dlna/__init__.py @@ -22,8 +22,7 @@ from async_upnp_client.exceptions import UpnpError, UpnpResponseError from async_upnp_client.profiles.dlna import DmrDevice, TransportState from async_upnp_client.search import async_search - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, CONF_ENTRY_ENABLE_ICY_METADATA, @@ -34,31 +33,26 @@ ConfigValueType, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import ( - ConfigEntryType, - PlayerFeature, - PlayerState, - PlayerType, -) -from music_assistant.common.models.errors import PlayerUnavailableError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.constants import CONF_ENFORCE_MP3, CONF_PLAYERS, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.didl_lite import create_didl_metadata -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant_models.enums import ConfigEntryType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import PlayerUnavailableError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia -from .helpers import DLNANotifyServer +from music_assistant.constants import CONF_ENFORCE_MP3, CONF_PLAYERS, VERBOSE_LOG_LEVEL +from music_assistant.helpers import DLNANotifyServer +from music_assistant.helpers.didl_lite import create_didl_metadata +from music_assistant.helpers.util import TaskManager +from music_assistant.models.player_provider import PlayerProvider if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Coroutine, Sequence from async_upnp_client.client import UpnpRequester, UpnpService, UpnpStateVariable from async_upnp_client.utils import CaseInsensitiveDict + from music_assistant_models.config_entries import PlayerConfig, ProviderConfig + from music_assistant_models.provider import ProviderManifest - from music_assistant.common.models.config_entries import PlayerConfig, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType PLAYER_CONFIG_ENTRIES = ( diff --git a/music_assistant/server/providers/dlna/helpers.py b/music_assistant/providers/dlna/helpers.py similarity index 96% rename from music_assistant/server/providers/dlna/helpers.py rename to music_assistant/providers/dlna/helpers.py index f59f98de7..32242ae85 100644 --- a/music_assistant/server/providers/dlna/helpers.py +++ b/music_assistant/providers/dlna/helpers.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from async_upnp_client.client import UpnpRequester - from music_assistant.server import MusicAssistant + from music_assistant import MusicAssistant class DLNANotifyServer(UpnpNotifyServer): diff --git a/music_assistant/server/providers/dlna/icon.svg b/music_assistant/providers/dlna/icon.svg similarity index 100% rename from music_assistant/server/providers/dlna/icon.svg rename to music_assistant/providers/dlna/icon.svg diff --git a/music_assistant/server/providers/dlna/manifest.json b/music_assistant/providers/dlna/manifest.json similarity index 100% rename from music_assistant/server/providers/dlna/manifest.json rename to music_assistant/providers/dlna/manifest.json diff --git a/music_assistant/server/providers/fanarttv/__init__.py b/music_assistant/providers/fanarttv/__init__.py similarity index 88% rename from music_assistant/server/providers/fanarttv/__init__.py rename to music_assistant/providers/fanarttv/__init__.py index cdb72c60b..eaaa533ba 100644 --- a/music_assistant/server/providers/fanarttv/__init__.py +++ b/music_assistant/providers/fanarttv/__init__.py @@ -6,21 +6,22 @@ from typing import TYPE_CHECKING import aiohttp.client_exceptions +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature +from music_assistant_models.media_items import ImageType, MediaItemImage, MediaItemMetadata -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature -from music_assistant.common.models.media_items import ImageType, MediaItemImage, MediaItemMetadata -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.metadata_provider import MetadataProvider +from music_assistant.controllers.cache import use_cache +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.metadata_provider import MetadataProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.media_items import Album, Artist - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.media_items import Album, Artist + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType SUPPORTED_FEATURES = ( ProviderFeature.ARTIST_METADATA, diff --git a/music_assistant/server/providers/fanarttv/manifest.json b/music_assistant/providers/fanarttv/manifest.json similarity index 100% rename from music_assistant/server/providers/fanarttv/manifest.json rename to music_assistant/providers/fanarttv/manifest.json diff --git a/music_assistant/server/providers/filesystem_local/__init__.py b/music_assistant/providers/filesystem_local/__init__.py similarity index 97% rename from music_assistant/server/providers/filesystem_local/__init__.py rename to music_assistant/providers/filesystem_local/__init__.py index 12c4d28d7..aa7f1927c 100644 --- a/music_assistant/server/providers/filesystem_local/__init__.py +++ b/music_assistant/providers/filesystem_local/__init__.py @@ -13,25 +13,10 @@ import shortuuid import xmltodict from aiofiles.os import wrap - -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( - MediaNotFoundError, - MusicAssistantError, - SetupFailedError, -) -from music_assistant.common.models.media_items import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import MediaNotFoundError, MusicAssistantError, SetupFailedError +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, @@ -49,7 +34,8 @@ UniqueList, is_track, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import ( CONF_PATH, DB_TABLE_ALBUM_ARTISTS, @@ -61,11 +47,11 @@ VARIOUS_ARTISTS_MBID, VARIOUS_ARTISTS_NAME, ) -from music_assistant.server.helpers.compare import compare_strings, create_safe_string -from music_assistant.server.helpers.playlists import parse_m3u, parse_pls -from music_assistant.server.helpers.tags import AudioTags, parse_tags, split_items -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.compare import compare_strings, create_safe_string +from music_assistant.helpers.playlists import parse_m3u, parse_pls +from music_assistant.helpers.tags import AudioTags, parse_tags, split_items +from music_assistant.helpers.util import TaskManager, parse_title_and_version +from music_assistant.models.music_provider import MusicProvider from .helpers import ( FileSystemItem, @@ -79,10 +65,11 @@ if TYPE_CHECKING: from collections.abc import AsyncGenerator - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 CONF_MISSING_ALBUM_ARTIST_ACTION = "missing_album_artist_action" diff --git a/music_assistant/server/providers/filesystem_local/helpers.py b/music_assistant/providers/filesystem_local/helpers.py similarity index 98% rename from music_assistant/server/providers/filesystem_local/helpers.py rename to music_assistant/providers/filesystem_local/helpers.py index 237699ab7..57e87b93d 100644 --- a/music_assistant/server/providers/filesystem_local/helpers.py +++ b/music_assistant/providers/filesystem_local/helpers.py @@ -6,7 +6,7 @@ import re from dataclasses import dataclass -from music_assistant.server.helpers.compare import compare_strings +from music_assistant.helpers.compare import compare_strings IGNORE_DIRS = ("recycle", "Recently-Snaphot") diff --git a/music_assistant/server/providers/filesystem_local/manifest.json b/music_assistant/providers/filesystem_local/manifest.json similarity index 100% rename from music_assistant/server/providers/filesystem_local/manifest.json rename to music_assistant/providers/filesystem_local/manifest.json diff --git a/music_assistant/server/providers/filesystem_smb/__init__.py b/music_assistant/providers/filesystem_smb/__init__.py similarity index 92% rename from music_assistant/server/providers/filesystem_smb/__init__.py rename to music_assistant/providers/filesystem_smb/__init__.py index 1168f2f14..b43245dd7 100644 --- a/music_assistant/server/providers/filesystem_smb/__init__.py +++ b/music_assistant/providers/filesystem_smb/__init__.py @@ -6,13 +6,14 @@ import platform from typing import TYPE_CHECKING -from music_assistant.common.helpers.util import get_ip_from_host -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.errors import LoginFailed +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.errors import LoginFailed + from music_assistant.constants import CONF_PASSWORD, CONF_USERNAME, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.process import check_output -from music_assistant.server.providers.filesystem_local import ( +from music_assistant.helpers.process import check_output +from music_assistant.helpers.util import get_ip_from_host +from music_assistant.providers.filesystem_local import ( CONF_ENTRY_MISSING_ALBUM_ARTIST, LocalFileSystemProvider, exists, @@ -20,10 +21,11 @@ ) if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 CONF_HOST = "host" CONF_SHARE = "share" diff --git a/music_assistant/server/providers/filesystem_smb/manifest.json b/music_assistant/providers/filesystem_smb/manifest.json similarity index 100% rename from music_assistant/server/providers/filesystem_smb/manifest.json rename to music_assistant/providers/filesystem_smb/manifest.json diff --git a/music_assistant/server/providers/fully_kiosk/__init__.py b/music_assistant/providers/fully_kiosk/__init__.py similarity index 91% rename from music_assistant/server/providers/fully_kiosk/__init__.py rename to music_assistant/providers/fully_kiosk/__init__.py index 1977ccd2d..4cbee92f8 100644 --- a/music_assistant/server/providers/fully_kiosk/__init__.py +++ b/music_assistant/providers/fully_kiosk/__init__.py @@ -8,8 +8,7 @@ from typing import TYPE_CHECKING from fullykiosk import FullyKiosk - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED, @@ -17,14 +16,10 @@ ConfigEntry, ConfigValueType, ) -from music_assistant.common.models.enums import ( - ConfigEntryType, - PlayerFeature, - PlayerState, - PlayerType, -) -from music_assistant.common.models.errors import PlayerUnavailableError, SetupFailedError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant_models.enums import ConfigEntryType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import PlayerUnavailableError, SetupFailedError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + from music_assistant.constants import ( CONF_ENFORCE_MP3, CONF_IP_ADDRESS, @@ -32,13 +27,14 @@ CONF_PORT, VERBOSE_LOG_LEVEL, ) -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant.models.player_provider import PlayerProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 AUDIOMANAGER_STREAM_MUSIC = 3 diff --git a/music_assistant/server/providers/fully_kiosk/manifest.json b/music_assistant/providers/fully_kiosk/manifest.json similarity index 100% rename from music_assistant/server/providers/fully_kiosk/manifest.json rename to music_assistant/providers/fully_kiosk/manifest.json diff --git a/music_assistant/server/providers/hass/__init__.py b/music_assistant/providers/hass/__init__.py similarity index 91% rename from music_assistant/server/providers/hass/__init__.py rename to music_assistant/providers/hass/__init__.py index a365b5a56..dc243eb43 100644 --- a/music_assistant/server/providers/hass/__init__.py +++ b/music_assistant/providers/hass/__init__.py @@ -23,19 +23,20 @@ get_token, get_websocket_url, ) +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType +from music_assistant_models.errors import LoginFailed, SetupFailedError -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType -from music_assistant.common.models.errors import LoginFailed, SetupFailedError from music_assistant.constants import MASS_LOGO_ONLINE -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.models.plugin import PluginProvider +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.models.plugin import PluginProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 DOMAIN = "hass" CONF_URL = "url" diff --git a/music_assistant/server/providers/hass/icon.svg b/music_assistant/providers/hass/icon.svg similarity index 100% rename from music_assistant/server/providers/hass/icon.svg rename to music_assistant/providers/hass/icon.svg diff --git a/music_assistant/server/providers/hass/manifest.json b/music_assistant/providers/hass/manifest.json similarity index 100% rename from music_assistant/server/providers/hass/manifest.json rename to music_assistant/providers/hass/manifest.json diff --git a/music_assistant/server/providers/hass_players/__init__.py b/music_assistant/providers/hass_players/__init__.py similarity index 95% rename from music_assistant/server/providers/hass_players/__init__.py rename to music_assistant/providers/hass_players/__init__.py index 2ed93adf1..cba0a6c87 100644 --- a/music_assistant/server/providers/hass_players/__init__.py +++ b/music_assistant/providers/hass_players/__init__.py @@ -12,9 +12,7 @@ from typing import TYPE_CHECKING, Any from hass_client.exceptions import FailedCommand - -from music_assistant.common.helpers.datetime import from_iso_string -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED, CONF_ENTRY_ENABLE_ICY_METADATA, @@ -25,16 +23,13 @@ ConfigValueOption, ConfigValueType, ) -from music_assistant.common.models.enums import ( - ConfigEntryType, - PlayerFeature, - PlayerState, - PlayerType, -) -from music_assistant.common.models.errors import SetupFailedError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.hass import DOMAIN as HASS_DOMAIN +from music_assistant_models.enums import ConfigEntryType, PlayerFeature, PlayerState, PlayerType +from music_assistant_models.errors import SetupFailedError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + +from music_assistant.helpers.datetime import from_iso_string +from music_assistant.models.player_provider import PlayerProvider +from music_assistant.providers.hass import DOMAIN as HASS_DOMAIN if TYPE_CHECKING: from collections.abc import AsyncGenerator @@ -43,12 +38,12 @@ from hass_client.models import Device as HassDevice from hass_client.models import Entity as HassEntity from hass_client.models import State as HassState + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - from music_assistant.server.providers.hass import HomeAssistant as HomeAssistantProvider + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + from music_assistant.providers.hass import HomeAssistant as HomeAssistantProvider CONF_PLAYERS = "players" diff --git a/music_assistant/server/providers/hass_players/icon.svg b/music_assistant/providers/hass_players/icon.svg similarity index 100% rename from music_assistant/server/providers/hass_players/icon.svg rename to music_assistant/providers/hass_players/icon.svg diff --git a/music_assistant/server/providers/hass_players/manifest.json b/music_assistant/providers/hass_players/manifest.json similarity index 100% rename from music_assistant/server/providers/hass_players/manifest.json rename to music_assistant/providers/hass_players/manifest.json diff --git a/music_assistant/server/providers/jellyfin/__init__.py b/music_assistant/providers/jellyfin/__init__.py similarity index 96% rename from music_assistant/server/providers/jellyfin/__init__.py rename to music_assistant/providers/jellyfin/__init__.py index b8abc700f..f6a03ed95 100644 --- a/music_assistant/server/providers/jellyfin/__init__.py +++ b/music_assistant/providers/jellyfin/__init__.py @@ -7,25 +7,21 @@ import uuid from asyncio import TaskGroup from collections.abc import AsyncGenerator +from typing import TYPE_CHECKING from aiojellyfin import MediaLibrary as JellyMediaLibrary from aiojellyfin import NotFound, SessionConfiguration, authenticate_by_name from aiojellyfin import Track as JellyTrack - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ( ConfigEntryType, ContentType, MediaType, ProviderFeature, StreamType, ) -from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( +from music_assistant_models.errors import LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, @@ -34,18 +30,18 @@ SearchResults, Track, ) -from music_assistant.common.models.provider import ProviderManifest -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant import MusicAssistant from music_assistant.constants import UNKNOWN_ARTIST_ID_MBID -from music_assistant.server.models import ProviderInstanceType -from music_assistant.server.models.music_provider import MusicProvider -from music_assistant.server.providers.jellyfin.parsers import ( +from music_assistant.models import ProviderInstanceType +from music_assistant.models.music_provider import MusicProvider +from music_assistant.providers.jellyfin.parsers import ( parse_album, parse_artist, parse_playlist, parse_track, ) -from music_assistant.server.server import MusicAssistant from .const import ( ALBUM_FIELDS, @@ -65,6 +61,9 @@ USER_APP_NAME, ) +if TYPE_CHECKING: + from music_assistant_models.provider import ProviderManifest + CONF_URL = "url" CONF_USERNAME = "username" CONF_PASSWORD = "password" diff --git a/music_assistant/server/providers/jellyfin/const.py b/music_assistant/providers/jellyfin/const.py similarity index 95% rename from music_assistant/server/providers/jellyfin/const.py rename to music_assistant/providers/jellyfin/const.py index 2bbfc9a9a..ea660c548 100644 --- a/music_assistant/server/providers/jellyfin/const.py +++ b/music_assistant/providers/jellyfin/const.py @@ -4,9 +4,9 @@ from aiojellyfin import ImageType as JellyImageType from aiojellyfin import ItemFields +from music_assistant_models.enums import ImageType, MediaType +from music_assistant_models.media_items import ItemMapping -from music_assistant.common.models.enums import ImageType, MediaType -from music_assistant.common.models.media_items import ItemMapping from music_assistant.constants import UNKNOWN_ARTIST DOMAIN: Final = "jellyfin" diff --git a/music_assistant/server/providers/jellyfin/icon.svg b/music_assistant/providers/jellyfin/icon.svg similarity index 100% rename from music_assistant/server/providers/jellyfin/icon.svg rename to music_assistant/providers/jellyfin/icon.svg diff --git a/music_assistant/server/providers/jellyfin/manifest.json b/music_assistant/providers/jellyfin/manifest.json similarity index 100% rename from music_assistant/server/providers/jellyfin/manifest.json rename to music_assistant/providers/jellyfin/manifest.json diff --git a/music_assistant/server/providers/jellyfin/parsers.py b/music_assistant/providers/jellyfin/parsers.py similarity index 98% rename from music_assistant/server/providers/jellyfin/parsers.py rename to music_assistant/providers/jellyfin/parsers.py index 1d022ce62..96545118d 100644 --- a/music_assistant/server/providers/jellyfin/parsers.py +++ b/music_assistant/providers/jellyfin/parsers.py @@ -7,10 +7,9 @@ from typing import TYPE_CHECKING from aiojellyfin import ImageType as JellyImageType - -from music_assistant.common.models.enums import ContentType, ExternalID, ImageType, MediaType -from music_assistant.common.models.errors import InvalidDataError -from music_assistant.common.models.media_items import ( +from music_assistant_models.enums import ContentType, ExternalID, ImageType, MediaType +from music_assistant_models.errors import InvalidDataError +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, diff --git a/music_assistant/server/providers/musicbrainz/__init__.py b/music_assistant/providers/musicbrainz/__init__.py similarity index 93% rename from music_assistant/server/providers/musicbrainz/__init__.py rename to music_assistant/providers/musicbrainz/__init__.py index 415226469..95918ad5f 100644 --- a/music_assistant/server/providers/musicbrainz/__init__.py +++ b/music_assistant/providers/musicbrainz/__init__.py @@ -12,26 +12,23 @@ from mashumaro import DataClassDictMixin from mashumaro.exceptions import MissingField +from music_assistant_models.enums import ExternalID, ProviderFeature +from music_assistant_models.errors import InvalidDataError, ResourceTemporarilyUnavailable -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.enums import ExternalID, ProviderFeature -from music_assistant.common.models.errors import InvalidDataError, ResourceTemporarilyUnavailable -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.helpers.compare import compare_strings -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.models.metadata_provider import MetadataProvider +from music_assistant.controllers.cache import use_cache +from music_assistant.helpers.compare import compare_strings +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.helpers.util import parse_title_and_version +from music_assistant.models.metadata_provider import MetadataProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, - ) - from music_assistant.common.models.media_items import Album, Track - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.media_items import Album, Track + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' diff --git a/music_assistant/server/providers/musicbrainz/icon.svg b/music_assistant/providers/musicbrainz/icon.svg similarity index 100% rename from music_assistant/server/providers/musicbrainz/icon.svg rename to music_assistant/providers/musicbrainz/icon.svg diff --git a/music_assistant/server/providers/musicbrainz/icon_dark.svg b/music_assistant/providers/musicbrainz/icon_dark.svg similarity index 100% rename from music_assistant/server/providers/musicbrainz/icon_dark.svg rename to music_assistant/providers/musicbrainz/icon_dark.svg diff --git a/music_assistant/server/providers/musicbrainz/manifest.json b/music_assistant/providers/musicbrainz/manifest.json similarity index 100% rename from music_assistant/server/providers/musicbrainz/manifest.json rename to music_assistant/providers/musicbrainz/manifest.json diff --git a/music_assistant/server/providers/opensubsonic/__init__.py b/music_assistant/providers/opensubsonic/__init__.py similarity index 88% rename from music_assistant/server/providers/opensubsonic/__init__.py rename to music_assistant/providers/opensubsonic/__init__.py index 4f6a42f6c..47e05ac5b 100644 --- a/music_assistant/server/providers/opensubsonic/__init__.py +++ b/music_assistant/providers/opensubsonic/__init__.py @@ -4,12 +4,9 @@ from typing import TYPE_CHECKING -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueType, - ProviderConfig, -) -from music_assistant.common.models.enums import ConfigEntryType +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig +from music_assistant_models.enums import ConfigEntryType + from music_assistant.constants import CONF_PASSWORD, CONF_PATH, CONF_PORT, CONF_USERNAME from .sonic_provider import ( @@ -20,9 +17,10 @@ ) if TYPE_CHECKING: - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType async def setup( diff --git a/music_assistant/server/providers/opensubsonic/icon.svg b/music_assistant/providers/opensubsonic/icon.svg similarity index 100% rename from music_assistant/server/providers/opensubsonic/icon.svg rename to music_assistant/providers/opensubsonic/icon.svg diff --git a/music_assistant/server/providers/opensubsonic/manifest.json b/music_assistant/providers/opensubsonic/manifest.json similarity index 100% rename from music_assistant/server/providers/opensubsonic/manifest.json rename to music_assistant/providers/opensubsonic/manifest.json diff --git a/music_assistant/server/providers/opensubsonic/sonic_provider.py b/music_assistant/providers/opensubsonic/sonic_provider.py similarity index 98% rename from music_assistant/server/providers/opensubsonic/sonic_provider.py rename to music_assistant/providers/opensubsonic/sonic_provider.py index 556cc2ba3..ed1c07a40 100644 --- a/music_assistant/server/providers/opensubsonic/sonic_provider.py +++ b/music_assistant/providers/opensubsonic/sonic_provider.py @@ -13,20 +13,15 @@ ParameterError, SonicError, ) - -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ContentType, ImageType, MediaType, ProviderFeature, StreamType, ) -from music_assistant.common.models.errors import ( - LoginFailed, - MediaNotFoundError, - ProviderPermissionDenied, -) -from music_assistant.common.models.media_items import ( +from music_assistant_models.errors import LoginFailed, MediaNotFoundError, ProviderPermissionDenied +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -38,7 +33,8 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import ( CONF_PASSWORD, CONF_PATH, @@ -46,7 +42,7 @@ CONF_USERNAME, UNKNOWN_ARTIST, ) -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable diff --git a/music_assistant/server/providers/player_group/__init__.py b/music_assistant/providers/player_group/__init__.py similarity index 97% rename from music_assistant/server/providers/player_group/__init__.py rename to music_assistant/providers/player_group/__init__.py index b3b5c6f06..8384da83e 100644 --- a/music_assistant/server/providers/player_group/__init__.py +++ b/music_assistant/providers/player_group/__init__.py @@ -14,8 +14,7 @@ import shortuuid from aiohttp import web - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( BASE_PLAYER_CONFIG_ENTRIES, CONF_ENTRY_CROSSFADE, CONF_ENTRY_CROSSFADE_DURATION, @@ -27,7 +26,7 @@ PlayerConfig, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ConfigEntryType, ContentType, EventType, @@ -37,14 +36,14 @@ PlayerType, ProviderFeature, ) -from music_assistant.common.models.errors import ( +from music_assistant_models.errors import ( PlayerUnavailableError, ProviderUnavailableError, UnsupportedFeaturedException, ) -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + from music_assistant.constants import ( CONF_CROSSFADE, CONF_CROSSFADE_DURATION, @@ -55,20 +54,22 @@ CONF_HTTP_PROFILE, CONF_SAMPLE_RATES, ) -from music_assistant.server.controllers.streams import DEFAULT_STREAM_HEADERS -from music_assistant.server.helpers.ffmpeg import get_ffmpeg_stream -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant.controllers.streams import DEFAULT_STREAM_HEADERS +from music_assistant.helpers.ffmpeg import get_ffmpeg_stream +from music_assistant.helpers.util import TaskManager +from music_assistant.models.player_provider import PlayerProvider from .ugp_stream import UGP_FORMAT, UGPStream if TYPE_CHECKING: from collections.abc import Iterable - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.event import MassEvent + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType # ruff: noqa: ARG002 diff --git a/music_assistant/server/providers/player_group/manifest.json b/music_assistant/providers/player_group/manifest.json similarity index 100% rename from music_assistant/server/providers/player_group/manifest.json rename to music_assistant/providers/player_group/manifest.json diff --git a/music_assistant/server/providers/player_group/ugp_stream.py b/music_assistant/providers/player_group/ugp_stream.py similarity index 92% rename from music_assistant/server/providers/player_group/ugp_stream.py rename to music_assistant/providers/player_group/ugp_stream.py index 281d80fb4..f5d8b8a18 100644 --- a/music_assistant/server/providers/player_group/ugp_stream.py +++ b/music_assistant/providers/player_group/ugp_stream.py @@ -10,10 +10,11 @@ import asyncio from collections.abc import AsyncGenerator, Awaitable, Callable -from music_assistant.common.helpers.util import empty_queue -from music_assistant.common.models.enums import ContentType -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.server.helpers.audio import get_ffmpeg_stream +from music_assistant_models.enums import ContentType +from music_assistant_models.media_items import AudioFormat + +from music_assistant.helpers.audio import get_ffmpeg_stream +from music_assistant.helpers.util import empty_queue # ruff: noqa: ARG002 diff --git a/music_assistant/server/providers/plex/__init__.py b/music_assistant/providers/plex/__init__.py similarity index 97% rename from music_assistant/server/providers/plex/__init__.py rename to music_assistant/providers/plex/__init__.py index 748b65bfc..5bf783d98 100644 --- a/music_assistant/server/providers/plex/__init__.py +++ b/music_assistant/providers/plex/__init__.py @@ -11,22 +11,13 @@ import plexapi.exceptions import requests -from plexapi.audio import Album as PlexAlbum -from plexapi.audio import Artist as PlexArtist -from plexapi.audio import Playlist as PlexPlaylist -from plexapi.audio import Track as PlexTrack -from plexapi.base import PlexObject -from plexapi.myplex import MyPlexAccount, MyPlexPinLogin -from plexapi.server import PlexServer - -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( ConfigEntry, ConfigValueOption, ConfigValueType, ProviderConfig, ) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ConfigEntryType, ContentType, ImageType, @@ -34,13 +25,13 @@ ProviderFeature, StreamType, ) -from music_assistant.common.models.errors import ( +from music_assistant_models.errors import ( InvalidDataError, LoginFailed, MediaNotFoundError, SetupFailedError, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, @@ -54,24 +45,33 @@ Track, UniqueList, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails +from plexapi.audio import Album as PlexAlbum +from plexapi.audio import Artist as PlexArtist +from plexapi.audio import Playlist as PlexPlaylist +from plexapi.audio import Track as PlexTrack +from plexapi.base import PlexObject +from plexapi.myplex import MyPlexAccount, MyPlexPinLogin +from plexapi.server import PlexServer + from music_assistant.constants import UNKNOWN_ARTIST -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.helpers.tags import parse_tags -from music_assistant.server.models.music_provider import MusicProvider -from music_assistant.server.providers.plex.helpers import discover_local_servers, get_libraries +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.tags import parse_tags +from music_assistant.helpers.util import parse_title_and_version +from music_assistant.models.music_provider import MusicProvider +from music_assistant.providers.plex.helpers import discover_local_servers, get_libraries if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable, Coroutine + from music_assistant_models.provider import ProviderManifest from plexapi.library import MusicSection as PlexMusicSection from plexapi.media import AudioStream as PlexAudioStream from plexapi.media import Media as PlexMedia from plexapi.media import MediaPart as PlexMediaPart - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType CONF_ACTION_AUTH_MYPLEX = "auth_myplex" CONF_ACTION_AUTH_LOCAL = "auth_local" diff --git a/music_assistant/server/providers/plex/helpers.py b/music_assistant/providers/plex/helpers.py similarity index 98% rename from music_assistant/server/providers/plex/helpers.py rename to music_assistant/providers/plex/helpers.py index af33e53db..0c303bcc9 100644 --- a/music_assistant/server/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.server import MusicAssistant + from music_assistant import MusicAssistant async def get_libraries( diff --git a/music_assistant/server/providers/plex/icon.svg b/music_assistant/providers/plex/icon.svg similarity index 100% rename from music_assistant/server/providers/plex/icon.svg rename to music_assistant/providers/plex/icon.svg diff --git a/music_assistant/server/providers/plex/manifest.json b/music_assistant/providers/plex/manifest.json similarity index 100% rename from music_assistant/server/providers/plex/manifest.json rename to music_assistant/providers/plex/manifest.json diff --git a/music_assistant/server/providers/qobuz/__init__.py b/music_assistant/providers/qobuz/__init__.py similarity index 96% rename from music_assistant/server/providers/qobuz/__init__.py rename to music_assistant/providers/qobuz/__init__.py index 33ebeb4fb..da407d365 100644 --- a/music_assistant/server/providers/qobuz/__init__.py +++ b/music_assistant/providers/qobuz/__init__.py @@ -9,23 +9,15 @@ from typing import TYPE_CHECKING from aiohttp import client_exceptions - -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.helpers.util import parse_title_and_version, try_parse_int -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import ( InvalidDataError, LoginFailed, MediaNotFoundError, ResourceTemporarilyUnavailable, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -40,25 +32,28 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import ( CONF_PASSWORD, CONF_USERNAME, VARIOUS_ARTISTS_MBID, VARIOUS_ARTISTS_NAME, ) -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.helpers.util import lock -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.helpers.util import lock, parse_title_and_version, try_parse_int +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: from collections.abc import AsyncGenerator - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 SUPPORTED_FEATURES = ( diff --git a/music_assistant/server/providers/qobuz/icon.svg b/music_assistant/providers/qobuz/icon.svg similarity index 100% rename from music_assistant/server/providers/qobuz/icon.svg rename to music_assistant/providers/qobuz/icon.svg diff --git a/music_assistant/server/providers/qobuz/icon_dark.svg b/music_assistant/providers/qobuz/icon_dark.svg similarity index 100% rename from music_assistant/server/providers/qobuz/icon_dark.svg rename to music_assistant/providers/qobuz/icon_dark.svg diff --git a/music_assistant/server/providers/qobuz/manifest.json b/music_assistant/providers/qobuz/manifest.json similarity index 100% rename from music_assistant/server/providers/qobuz/manifest.json rename to music_assistant/providers/qobuz/manifest.json diff --git a/music_assistant/server/providers/radiobrowser/__init__.py b/music_assistant/providers/radiobrowser/__init__.py similarity index 93% rename from music_assistant/server/providers/radiobrowser/__init__.py rename to music_assistant/providers/radiobrowser/__init__.py index 5675f4c6d..203cb4495 100644 --- a/music_assistant/server/providers/radiobrowser/__init__.py +++ b/music_assistant/providers/radiobrowser/__init__.py @@ -5,17 +5,10 @@ from collections.abc import AsyncGenerator, Sequence from typing import TYPE_CHECKING, cast -from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station - -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ( - ConfigEntryType, - LinkType, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import MediaNotFoundError -from music_assistant.common.models.media_items import ( +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ConfigEntryType, LinkType, ProviderFeature, StreamType +from music_assistant_models.errors import MediaNotFoundError +from music_assistant_models.media_items import ( AudioFormat, BrowseFolder, ContentType, @@ -29,9 +22,11 @@ SearchResults, UniqueList, ) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant_models.streamdetails import StreamDetails +from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station + +from music_assistant.controllers.cache import use_cache +from music_assistant.models.music_provider import MusicProvider SUPPORTED_FEATURES = ( ProviderFeature.SEARCH, @@ -44,10 +39,11 @@ ) if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType CONF_STORED_RADIOS = "stored_radios" diff --git a/music_assistant/server/providers/radiobrowser/manifest.json b/music_assistant/providers/radiobrowser/manifest.json similarity index 100% rename from music_assistant/server/providers/radiobrowser/manifest.json rename to music_assistant/providers/radiobrowser/manifest.json diff --git a/music_assistant/server/providers/siriusxm/__init__.py b/music_assistant/providers/siriusxm/__init__.py similarity index 92% rename from music_assistant/server/providers/siriusxm/__init__.py rename to music_assistant/providers/siriusxm/__init__.py index b4c04f00f..f362c9a69 100644 --- a/music_assistant/server/providers/siriusxm/__init__.py +++ b/music_assistant/providers/siriusxm/__init__.py @@ -5,13 +5,8 @@ from collections.abc import AsyncGenerator, Awaitable, Sequence from typing import TYPE_CHECKING, Any, cast -from music_assistant.common.helpers.util import select_free_port -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( ConfigEntryType, ContentType, LinkType, @@ -19,8 +14,8 @@ ProviderFeature, StreamType, ) -from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( +from music_assistant_models.errors import LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( AudioFormat, ImageType, ItemMapping, @@ -30,15 +25,18 @@ ProviderMapping, Radio, ) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.webserver import Webserver -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant_models.streamdetails import StreamDetails + +from music_assistant.helpers.util import select_free_port +from music_assistant.helpers.webserver import Webserver +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 import sxm.http from sxm import SXMClientAsync diff --git a/music_assistant/server/providers/siriusxm/icon.svg b/music_assistant/providers/siriusxm/icon.svg similarity index 100% rename from music_assistant/server/providers/siriusxm/icon.svg rename to music_assistant/providers/siriusxm/icon.svg diff --git a/music_assistant/server/providers/siriusxm/icon_dark.svg b/music_assistant/providers/siriusxm/icon_dark.svg similarity index 100% rename from music_assistant/server/providers/siriusxm/icon_dark.svg rename to music_assistant/providers/siriusxm/icon_dark.svg diff --git a/music_assistant/server/providers/siriusxm/manifest.json b/music_assistant/providers/siriusxm/manifest.json similarity index 100% rename from music_assistant/server/providers/siriusxm/manifest.json rename to music_assistant/providers/siriusxm/manifest.json diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/providers/slimproto/__init__.py similarity index 97% rename from music_assistant/server/providers/slimproto/__init__.py rename to music_assistant/providers/slimproto/__init__.py index f78d2b9ab..fc9aaffbd 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/providers/slimproto/__init__.py @@ -19,8 +19,7 @@ from aioslimproto.models import Preset as SlimPreset from aioslimproto.models import VisualisationType as SlimVisualisationType from aioslimproto.server import SlimServer - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_ENFORCE_MP3, @@ -36,7 +35,7 @@ PlayerConfig, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ConfigEntryType, ContentType, MediaType, @@ -46,9 +45,10 @@ ProviderFeature, RepeatMode, ) -from music_assistant.common.models.errors import MusicAssistantError, SetupFailedError -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant_models.errors import MusicAssistantError, SetupFailedError +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + from music_assistant.constants import ( CONF_CROSSFADE, CONF_CROSSFADE_DURATION, @@ -57,20 +57,20 @@ CONF_SYNC_ADJUST, VERBOSE_LOG_LEVEL, ) -from music_assistant.server.helpers.audio import get_ffmpeg_stream, get_player_filter_params -from music_assistant.server.helpers.util import TaskManager -from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.player_group import PlayerGroupProvider +from music_assistant.helpers.audio import get_ffmpeg_stream, get_player_filter_params +from music_assistant.helpers.util import TaskManager +from music_assistant.models.player_provider import PlayerProvider +from music_assistant.providers.player_group import PlayerGroupProvider from .multi_client_stream import MultiClientStream if TYPE_CHECKING: from aioslimproto.models import SlimEvent + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType CACHE_KEY_PREV_STATE = "slimproto_prev_state" diff --git a/music_assistant/server/providers/slimproto/icon.svg b/music_assistant/providers/slimproto/icon.svg similarity index 100% rename from music_assistant/server/providers/slimproto/icon.svg rename to music_assistant/providers/slimproto/icon.svg diff --git a/music_assistant/server/providers/slimproto/manifest.json b/music_assistant/providers/slimproto/manifest.json similarity index 100% rename from music_assistant/server/providers/slimproto/manifest.json rename to music_assistant/providers/slimproto/manifest.json diff --git a/music_assistant/server/providers/slimproto/multi_client_stream.py b/music_assistant/providers/slimproto/multi_client_stream.py similarity index 94% rename from music_assistant/server/providers/slimproto/multi_client_stream.py rename to music_assistant/providers/slimproto/multi_client_stream.py index 5ba2a53b3..11acf23c0 100644 --- a/music_assistant/server/providers/slimproto/multi_client_stream.py +++ b/music_assistant/providers/slimproto/multi_client_stream.py @@ -5,9 +5,10 @@ from collections.abc import AsyncGenerator from contextlib import suppress -from music_assistant.common.helpers.util import empty_queue -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.server.helpers.audio import get_ffmpeg_stream +from music_assistant_models.media_items import AudioFormat + +from music_assistant.helpers.audio import get_ffmpeg_stream +from music_assistant.helpers.util import empty_queue LOGGER = logging.getLogger(__name__) diff --git a/music_assistant/server/providers/snapcast/__init__.py b/music_assistant/providers/snapcast/__init__.py similarity index 96% rename from music_assistant/server/providers/snapcast/__init__.py rename to music_assistant/providers/snapcast/__init__.py index 0cc1b5238..f21519053 100644 --- a/music_assistant/server/providers/snapcast/__init__.py +++ b/music_assistant/providers/snapcast/__init__.py @@ -13,13 +13,7 @@ from typing import TYPE_CHECKING, Final, cast from bidict import bidict -from snapcast.control import create_server -from snapcast.control.client import Snapclient -from zeroconf import NonUniqueNameException -from zeroconf.asyncio import AsyncServiceInfo - -from music_assistant.common.helpers.util import get_ip_pton -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_CROSSFADE_DURATION, CONF_ENTRY_FLOW_MODE_ENFORCED, @@ -28,7 +22,7 @@ ConfigValueType, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ConfigEntryType, ContentType, MediaType, @@ -37,23 +31,29 @@ PlayerType, ProviderFeature, ) -from music_assistant.common.models.errors import SetupFailedError -from music_assistant.common.models.media_items import AudioFormat -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia -from music_assistant.server.helpers.audio import FFMpeg, get_ffmpeg_stream, get_player_filter_params -from music_assistant.server.helpers.process import AsyncProcess, check_output -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant_models.errors import SetupFailedError +from music_assistant_models.media_items import AudioFormat +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from snapcast.control import create_server +from snapcast.control.client import Snapclient +from zeroconf import NonUniqueNameException +from zeroconf.asyncio import AsyncServiceInfo + +from music_assistant.helpers.audio import FFMpeg, get_ffmpeg_stream, get_player_filter_params +from music_assistant.helpers.process import AsyncProcess, check_output +from music_assistant.helpers.util import get_ip_pton +from music_assistant.models.player_provider import PlayerProvider if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest from snapcast.control.group import Snapgroup from snapcast.control.server import Snapserver from snapcast.control.stream import Snapstream - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType - from music_assistant.server.providers.player_group import PlayerGroupProvider + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType + from music_assistant.providers.player_group import PlayerGroupProvider CONF_SERVER_HOST = "snapcast_server_host" CONF_SERVER_CONTROL_PORT = "snapcast_server_control_port" diff --git a/music_assistant/server/providers/snapcast/icon.svg b/music_assistant/providers/snapcast/icon.svg similarity index 100% rename from music_assistant/server/providers/snapcast/icon.svg rename to music_assistant/providers/snapcast/icon.svg diff --git a/music_assistant/server/providers/snapcast/manifest.json b/music_assistant/providers/snapcast/manifest.json similarity index 100% rename from music_assistant/server/providers/snapcast/manifest.json rename to music_assistant/providers/snapcast/manifest.json diff --git a/music_assistant/server/providers/snapcast/snapweb/10-seconds-of-silence.mp3 b/music_assistant/providers/snapcast/snapweb/10-seconds-of-silence.mp3 similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/10-seconds-of-silence.mp3 rename to music_assistant/providers/snapcast/snapweb/10-seconds-of-silence.mp3 diff --git a/music_assistant/server/providers/snapcast/snapweb/3rd-party/libflac.js b/music_assistant/providers/snapcast/snapweb/3rd-party/libflac.js similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/3rd-party/libflac.js rename to music_assistant/providers/snapcast/snapweb/3rd-party/libflac.js diff --git a/music_assistant/server/providers/snapcast/snapweb/config.js b/music_assistant/providers/snapcast/snapweb/config.js similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/config.js rename to music_assistant/providers/snapcast/snapweb/config.js diff --git a/music_assistant/server/providers/snapcast/snapweb/favicon.ico b/music_assistant/providers/snapcast/snapweb/favicon.ico similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/favicon.ico rename to music_assistant/providers/snapcast/snapweb/favicon.ico diff --git a/music_assistant/server/providers/snapcast/snapweb/index.html b/music_assistant/providers/snapcast/snapweb/index.html similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/index.html rename to music_assistant/providers/snapcast/snapweb/index.html diff --git a/music_assistant/server/providers/snapcast/snapweb/launcher-icon.png b/music_assistant/providers/snapcast/snapweb/launcher-icon.png similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/launcher-icon.png rename to music_assistant/providers/snapcast/snapweb/launcher-icon.png diff --git a/music_assistant/server/providers/snapcast/snapweb/manifest.json b/music_assistant/providers/snapcast/snapweb/manifest.json similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/manifest.json rename to music_assistant/providers/snapcast/snapweb/manifest.json diff --git a/music_assistant/server/providers/snapcast/snapweb/mute_icon.png b/music_assistant/providers/snapcast/snapweb/mute_icon.png similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/mute_icon.png rename to music_assistant/providers/snapcast/snapweb/mute_icon.png diff --git a/music_assistant/server/providers/snapcast/snapweb/play.png b/music_assistant/providers/snapcast/snapweb/play.png similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/play.png rename to music_assistant/providers/snapcast/snapweb/play.png diff --git a/music_assistant/server/providers/snapcast/snapweb/snapcast-512.png b/music_assistant/providers/snapcast/snapweb/snapcast-512.png similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/snapcast-512.png rename to music_assistant/providers/snapcast/snapweb/snapcast-512.png diff --git a/music_assistant/server/providers/snapcast/snapweb/snapcontrol.js b/music_assistant/providers/snapcast/snapweb/snapcontrol.js similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/snapcontrol.js rename to music_assistant/providers/snapcast/snapweb/snapcontrol.js diff --git a/music_assistant/server/providers/snapcast/snapweb/snapstream.js b/music_assistant/providers/snapcast/snapweb/snapstream.js similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/snapstream.js rename to music_assistant/providers/snapcast/snapweb/snapstream.js diff --git a/music_assistant/server/providers/snapcast/snapweb/speaker_icon.png b/music_assistant/providers/snapcast/snapweb/speaker_icon.png similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/speaker_icon.png rename to music_assistant/providers/snapcast/snapweb/speaker_icon.png diff --git a/music_assistant/server/providers/snapcast/snapweb/stop.png b/music_assistant/providers/snapcast/snapweb/stop.png similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/stop.png rename to music_assistant/providers/snapcast/snapweb/stop.png diff --git a/music_assistant/server/providers/snapcast/snapweb/styles.css b/music_assistant/providers/snapcast/snapweb/styles.css similarity index 100% rename from music_assistant/server/providers/snapcast/snapweb/styles.css rename to music_assistant/providers/snapcast/snapweb/styles.css diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/providers/sonos/__init__.py similarity index 80% rename from music_assistant/server/providers/sonos/__init__.py rename to music_assistant/providers/sonos/__init__.py index bdc900d9f..beb9361f4 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/providers/sonos/__init__.py @@ -10,16 +10,16 @@ import logging from typing import TYPE_CHECKING -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType from music_assistant.constants import VERBOSE_LOG_LEVEL from .provider import SonosPlayerProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType async def setup( diff --git a/music_assistant/server/providers/sonos/const.py b/music_assistant/providers/sonos/const.py similarity index 90% rename from music_assistant/server/providers/sonos/const.py rename to music_assistant/providers/sonos/const.py index 75ff29337..1daa076ad 100644 --- a/music_assistant/server/providers/sonos/const.py +++ b/music_assistant/providers/sonos/const.py @@ -3,8 +3,7 @@ from __future__ import annotations from aiosonos.api.models import PlayBackState as SonosPlayBackState - -from music_assistant.common.models.enums import PlayerFeature, PlayerState +from music_assistant_models.enums import PlayerFeature, PlayerState PLAYBACK_STATE_MAP = { SonosPlayBackState.PLAYBACK_STATE_BUFFERING: PlayerState.PLAYING, diff --git a/music_assistant/server/providers/sonos/helpers.py b/music_assistant/providers/sonos/helpers.py similarity index 100% rename from music_assistant/server/providers/sonos/helpers.py rename to music_assistant/providers/sonos/helpers.py diff --git a/music_assistant/server/providers/sonos/icon.svg b/music_assistant/providers/sonos/icon.svg similarity index 100% rename from music_assistant/server/providers/sonos/icon.svg rename to music_assistant/providers/sonos/icon.svg diff --git a/music_assistant/server/providers/sonos/manifest.json b/music_assistant/providers/sonos/manifest.json similarity index 100% rename from music_assistant/server/providers/sonos/manifest.json rename to music_assistant/providers/sonos/manifest.json diff --git a/music_assistant/server/providers/sonos/player.py b/music_assistant/providers/sonos/player.py similarity index 99% rename from music_assistant/server/providers/sonos/player.py rename to music_assistant/providers/sonos/player.py index 45bb672d4..9af19073e 100644 --- a/music_assistant/server/providers/sonos/player.py +++ b/music_assistant/providers/sonos/player.py @@ -22,16 +22,15 @@ from aiosonos.const import EventType as SonosEventType from aiosonos.const import SonosEvent from aiosonos.exceptions import ConnectionFailed, FailedCommand - -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( EventType, PlayerFeature, PlayerState, PlayerType, RepeatMode, ) -from music_assistant.common.models.event import MassEvent -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia + from music_assistant.constants import CONF_CROSSFADE from .const import ( @@ -46,6 +45,7 @@ if TYPE_CHECKING: from aiosonos.api.models import DiscoveryInfo as SonosDiscoveryInfo + from music_assistant_models.event import MassEvent from .provider import SonosPlayerProvider diff --git a/music_assistant/server/providers/sonos/provider.py b/music_assistant/providers/sonos/provider.py similarity index 98% rename from music_assistant/server/providers/sonos/provider.py rename to music_assistant/providers/sonos/provider.py index 2e95f68ba..9a611df06 100644 --- a/music_assistant/server/providers/sonos/provider.py +++ b/music_assistant/providers/sonos/provider.py @@ -16,24 +16,24 @@ from aiohttp.client_exceptions import ClientError from aiosonos.api.models import SonosCapability from aiosonos.utils import get_discovery_info -from zeroconf import ServiceStateChange - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_ENFORCE_MP3, CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, ConfigEntry, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import ConfigEntryType, ContentType, ProviderFeature -from music_assistant.common.models.errors import PlayerCommandFailed -from music_assistant.common.models.player import DeviceInfo, PlayerMedia +from music_assistant_models.enums import ConfigEntryType, ContentType, ProviderFeature +from music_assistant_models.errors import PlayerCommandFailed +from music_assistant_models.player import DeviceInfo, PlayerMedia +from zeroconf import ServiceStateChange + from music_assistant.constants import MASS_LOGO_ONLINE, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.tags import parse_tags -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant.helpers import get_primary_ip_address +from music_assistant.helpers.tags import parse_tags +from music_assistant.models.player_provider import PlayerProvider from .const import CONF_AIRPLAY_MODE -from .helpers import get_primary_ip_address from .player import SonosPlayer if TYPE_CHECKING: diff --git a/music_assistant/server/providers/sonos_s1/__init__.py b/music_assistant/providers/sonos_s1/__init__.py similarity index 96% rename from music_assistant/server/providers/sonos_s1/__init__.py rename to music_assistant/providers/sonos_s1/__init__.py index e16042fa5..d983d7606 100644 --- a/music_assistant/server/providers/sonos_s1/__init__.py +++ b/music_assistant/providers/sonos_s1/__init__.py @@ -15,12 +15,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING -from requests.exceptions import RequestException -from soco import config as soco_config -from soco import events_asyncio, zonegroupstate -from soco.discovery import discover, scan_network - -from music_assistant.common.models.config_entries import ( +from music_assistant_models.config_entries import ( CONF_ENTRY_CROSSFADE, CONF_ENTRY_ENFORCE_MP3, CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED, @@ -29,28 +24,33 @@ ConfigValueType, create_sample_rates_config_entry, ) -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ConfigEntryType, PlayerFeature, PlayerState, PlayerType, ProviderFeature, ) -from music_assistant.common.models.errors import PlayerCommandFailed, PlayerUnavailableError -from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia +from music_assistant_models.errors import PlayerCommandFailed, PlayerUnavailableError +from music_assistant_models.player import DeviceInfo, Player, PlayerMedia +from requests.exceptions import RequestException +from soco import config as soco_config +from soco import events_asyncio, zonegroupstate +from soco.discovery import discover, scan_network + from music_assistant.constants import CONF_CROSSFADE, CONF_ENFORCE_MP3, VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.didl_lite import create_didl_metadata -from music_assistant.server.models.player_provider import PlayerProvider +from music_assistant.helpers.didl_lite import create_didl_metadata +from music_assistant.models.player_provider import PlayerProvider from .player import SonosPlayer if TYPE_CHECKING: + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest from soco.core import SoCo - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType PLAYER_FEATURES = ( diff --git a/music_assistant/server/providers/sonos_s1/helpers.py b/music_assistant/providers/sonos_s1/helpers.py similarity index 98% rename from music_assistant/server/providers/sonos_s1/helpers.py rename to music_assistant/providers/sonos_s1/helpers.py index 662cf6c40..b94765622 100644 --- a/music_assistant/server/providers/sonos_s1/helpers.py +++ b/music_assistant/providers/sonos_s1/helpers.py @@ -6,11 +6,10 @@ from collections.abc import Callable from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload +from music_assistant_models.errors import PlayerCommandFailed from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException -from music_assistant.common.models.errors import PlayerCommandFailed - if TYPE_CHECKING: from . import SonosPlayer diff --git a/music_assistant/server/providers/sonos_s1/icon.png b/music_assistant/providers/sonos_s1/icon.png similarity index 100% rename from music_assistant/server/providers/sonos_s1/icon.png rename to music_assistant/providers/sonos_s1/icon.png diff --git a/music_assistant/server/providers/sonos_s1/icon.svg b/music_assistant/providers/sonos_s1/icon.svg similarity index 100% rename from music_assistant/server/providers/sonos_s1/icon.svg rename to music_assistant/providers/sonos_s1/icon.svg diff --git a/music_assistant/server/providers/sonos_s1/manifest.json b/music_assistant/providers/sonos_s1/manifest.json similarity index 100% rename from music_assistant/server/providers/sonos_s1/manifest.json rename to music_assistant/providers/sonos_s1/manifest.json diff --git a/music_assistant/server/providers/sonos_s1/player.py b/music_assistant/providers/sonos_s1/player.py similarity index 98% rename from music_assistant/server/providers/sonos_s1/player.py rename to music_assistant/providers/sonos_s1/player.py index 600e49709..29e42c3bd 100644 --- a/music_assistant/server/providers/sonos_s1/player.py +++ b/music_assistant/providers/sonos_s1/player.py @@ -15,6 +15,9 @@ from collections.abc import Callable, Coroutine from typing import TYPE_CHECKING, Any +from music_assistant_models.enums import PlayerFeature, PlayerState +from music_assistant_models.errors import PlayerCommandFailed +from music_assistant_models.player import DeviceInfo, Player from soco import SoCoException from soco.core import ( MUSIC_SRC_AIRPLAY, @@ -26,13 +29,9 @@ ) from soco.data_structures import DidlAudioBroadcast, DidlPlaylistContainer -from music_assistant.common.helpers.datetime import utc -from music_assistant.common.models.enums import PlayerFeature, PlayerState -from music_assistant.common.models.errors import PlayerCommandFailed -from music_assistant.common.models.player import DeviceInfo, Player from music_assistant.constants import VERBOSE_LOG_LEVEL - -from .helpers import SonosUpdateError, soco_error +from music_assistant.helpers import SonosUpdateError, soco_error +from music_assistant.helpers.datetime import utc if TYPE_CHECKING: from soco.events_base import Event as SonosEvent diff --git a/music_assistant/server/providers/soundcloud/__init__.py b/music_assistant/providers/soundcloud/__init__.py similarity index 95% rename from music_assistant/server/providers/soundcloud/__init__.py rename to music_assistant/providers/soundcloud/__init__.py index 3b6bae568..36013ca19 100644 --- a/music_assistant/server/providers/soundcloud/__init__.py +++ b/music_assistant/providers/soundcloud/__init__.py @@ -6,13 +6,10 @@ import time from typing import TYPE_CHECKING -from soundcloudpy import SoundcloudAsyncAPI - -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature, StreamType -from music_assistant.common.models.errors import InvalidDataError, LoginFailed -from music_assistant.common.models.media_items import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ProviderFeature, StreamType +from music_assistant_models.errors import InvalidDataError, LoginFailed +from music_assistant_models.media_items import ( Artist, AudioFormat, ContentType, @@ -24,8 +21,11 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant_models.streamdetails import StreamDetails +from soundcloudpy import SoundcloudAsyncAPI + +from music_assistant.helpers.util import parse_title_and_version +from music_assistant.models.music_provider import MusicProvider CONF_CLIENT_ID = "client_id" CONF_AUTHORIZATION = "authorization" @@ -44,10 +44,11 @@ if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 async def setup( diff --git a/music_assistant/server/providers/soundcloud/icon.svg b/music_assistant/providers/soundcloud/icon.svg similarity index 100% rename from music_assistant/server/providers/soundcloud/icon.svg rename to music_assistant/providers/soundcloud/icon.svg diff --git a/music_assistant/server/providers/soundcloud/manifest.json b/music_assistant/providers/soundcloud/manifest.json similarity index 100% rename from music_assistant/server/providers/soundcloud/manifest.json rename to music_assistant/providers/soundcloud/manifest.json diff --git a/music_assistant/server/providers/spotify/__init__.py b/music_assistant/providers/spotify/__init__.py similarity index 96% rename from music_assistant/server/providers/spotify/__init__.py rename to music_assistant/providers/spotify/__init__.py index a3128e566..3789a194c 100644 --- a/music_assistant/server/providers/spotify/__init__.py +++ b/music_assistant/providers/spotify/__init__.py @@ -10,23 +10,16 @@ from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlencode -from music_assistant.common.helpers.json import json_loads -from music_assistant.common.helpers.util import parse_title_and_version -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ( - ConfigEntryType, - ExternalID, - ProviderFeature, - StreamType, -) -from music_assistant.common.models.errors import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature, StreamType +from music_assistant_models.errors import ( AudioError, LoginFailed, MediaNotFoundError, ResourceTemporarilyUnavailable, SetupFailedError, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -41,23 +34,26 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import VERBOSE_LOG_LEVEL -from music_assistant.server.helpers.app_vars import app_var -from music_assistant.server.helpers.audio import get_chunksize -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.helpers.process import AsyncProcess, check_output -from music_assistant.server.helpers.throttle_retry import ThrottlerManager, throttle_with_retries -from music_assistant.server.helpers.util import lock -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.app_vars import app_var +from music_assistant.helpers.audio import get_chunksize +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.json import json_loads +from music_assistant.helpers.process import AsyncProcess, check_output +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.helpers.util import lock, parse_title_and_version +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: from collections.abc import AsyncGenerator - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 CONF_CLIENT_ID = "client_id" CONF_ACTION_AUTH = "auth" diff --git a/music_assistant/server/providers/spotify/bin/librespot-linux-aarch64 b/music_assistant/providers/spotify/bin/librespot-linux-aarch64 similarity index 100% rename from music_assistant/server/providers/spotify/bin/librespot-linux-aarch64 rename to music_assistant/providers/spotify/bin/librespot-linux-aarch64 diff --git a/music_assistant/server/providers/spotify/bin/librespot-linux-x86_64 b/music_assistant/providers/spotify/bin/librespot-linux-x86_64 similarity index 100% rename from music_assistant/server/providers/spotify/bin/librespot-linux-x86_64 rename to music_assistant/providers/spotify/bin/librespot-linux-x86_64 diff --git a/music_assistant/server/providers/spotify/bin/librespot-macos-arm64 b/music_assistant/providers/spotify/bin/librespot-macos-arm64 similarity index 100% rename from music_assistant/server/providers/spotify/bin/librespot-macos-arm64 rename to music_assistant/providers/spotify/bin/librespot-macos-arm64 diff --git a/music_assistant/server/providers/spotify/icon.svg b/music_assistant/providers/spotify/icon.svg similarity index 100% rename from music_assistant/server/providers/spotify/icon.svg rename to music_assistant/providers/spotify/icon.svg diff --git a/music_assistant/server/providers/spotify/manifest.json b/music_assistant/providers/spotify/manifest.json similarity index 100% rename from music_assistant/server/providers/spotify/manifest.json rename to music_assistant/providers/spotify/manifest.json diff --git a/music_assistant/server/providers/test/__init__.py b/music_assistant/providers/test/__init__.py similarity index 89% rename from music_assistant/server/providers/test/__init__.py rename to music_assistant/providers/test/__init__.py index db3eabb66..630f64bc6 100644 --- a/music_assistant/server/providers/test/__init__.py +++ b/music_assistant/providers/test/__init__.py @@ -5,15 +5,14 @@ from collections.abc import AsyncGenerator from typing import TYPE_CHECKING -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ( +from music_assistant_models.enums import ( ContentType, ImageType, MediaType, ProviderFeature, StreamType, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, @@ -23,15 +22,17 @@ Track, UniqueList, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import MASS_LOGO, VARIOUS_ARTISTS_FANART -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigEntry, ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType DEFAULT_THUMB = MediaItemImage( diff --git a/music_assistant/server/providers/test/icon.svg b/music_assistant/providers/test/icon.svg similarity index 100% rename from music_assistant/server/providers/test/icon.svg rename to music_assistant/providers/test/icon.svg diff --git a/music_assistant/server/providers/test/manifest.json b/music_assistant/providers/test/manifest.json similarity index 100% rename from music_assistant/server/providers/test/manifest.json rename to music_assistant/providers/test/manifest.json diff --git a/music_assistant/server/providers/theaudiodb/__init__.py b/music_assistant/providers/theaudiodb/__init__.py similarity index 94% rename from music_assistant/server/providers/theaudiodb/__init__.py rename to music_assistant/providers/theaudiodb/__init__.py index c786aaa9b..ac37764ae 100644 --- a/music_assistant/server/providers/theaudiodb/__init__.py +++ b/music_assistant/providers/theaudiodb/__init__.py @@ -6,10 +6,9 @@ from typing import TYPE_CHECKING, Any, cast import aiohttp.client_exceptions - -from music_assistant.common.models.config_entries import ConfigEntry -from music_assistant.common.models.enums import ConfigEntryType, ExternalID, ProviderFeature -from music_assistant.common.models.media_items import ( +from music_assistant_models.config_entries import ConfigEntry +from music_assistant_models.enums import ConfigEntryType, ExternalID, ProviderFeature +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -21,17 +20,19 @@ Track, UniqueList, ) -from music_assistant.server.controllers.cache import use_cache -from music_assistant.server.helpers.app_vars import app_var # type: ignore[attr-defined] -from music_assistant.server.helpers.compare import compare_strings -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.metadata_provider import MetadataProvider + +from music_assistant.controllers.cache import use_cache +from music_assistant.helpers.app_vars import app_var # type: ignore[attr-defined] +from music_assistant.helpers.compare import compare_strings +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.metadata_provider import MetadataProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ConfigValueType, ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant_models.config_entries import ConfigValueType, ProviderConfig + from music_assistant_models.provider import ProviderManifest + + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType SUPPORTED_FEATURES = ( ProviderFeature.ARTIST_METADATA, @@ -323,7 +324,7 @@ async def __parse_album(self, album: Album, adb_album: dict[str, Any]) -> MediaI album_artist.mbid = adb_album["strMusicBrainzArtistID"] await self.mass.music.artists.update_item_in_library( album_artist.item_id, - album_artist, # type: ignore[arg-type] + album_artist, ) return metadata @@ -369,7 +370,7 @@ async def __parse_track(self, track: Track, adb_track: dict[str, Any]) -> MediaI album_artist.mbid = adb_track["strMusicBrainzArtistID"] await self.mass.music.artists.update_item_in_library( album_artist.item_id, - album_artist, # type: ignore[arg-type] + album_artist, ) # update the album mbid while at it if ( diff --git a/music_assistant/server/providers/theaudiodb/manifest.json b/music_assistant/providers/theaudiodb/manifest.json similarity index 100% rename from music_assistant/server/providers/theaudiodb/manifest.json rename to music_assistant/providers/theaudiodb/manifest.json diff --git a/music_assistant/server/providers/tidal/__init__.py b/music_assistant/providers/tidal/__init__.py similarity index 97% rename from music_assistant/server/providers/tidal/__init__.py rename to music_assistant/providers/tidal/__init__.py index f06defcae..b42725cb4 100644 --- a/music_assistant/server/providers/tidal/__init__.py +++ b/music_assistant/providers/tidal/__init__.py @@ -11,20 +11,8 @@ from enum import StrEnum from typing import TYPE_CHECKING, ParamSpec, TypeVar, cast -from tidalapi import Album as TidalAlbum -from tidalapi import Artist as TidalArtist -from tidalapi import Config as TidalConfig -from tidalapi import Playlist as TidalPlaylist -from tidalapi import Session as TidalSession -from tidalapi import Track as TidalTrack -from tidalapi import exceptions as tidal_exceptions - -from music_assistant.common.models.config_entries import ( - ConfigEntry, - ConfigValueOption, - ConfigValueType, -) -from music_assistant.common.models.enums import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueOption, ConfigValueType +from music_assistant_models.enums import ( AlbumType, CacheCategory, ConfigEntryType, @@ -34,8 +22,8 @@ ProviderFeature, StreamType, ) -from music_assistant.common.models.errors import LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( +from music_assistant_models.errors import LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( Album, Artist, AudioFormat, @@ -49,14 +37,19 @@ Track, UniqueList, ) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.helpers.tags import AudioTags, parse_tags -from music_assistant.server.helpers.throttle_retry import ( - ThrottlerManager, - throttle_with_retries, -) -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant_models.streamdetails import StreamDetails +from tidalapi import Album as TidalAlbum +from tidalapi import Artist as TidalArtist +from tidalapi import Config as TidalConfig +from tidalapi import Playlist as TidalPlaylist +from tidalapi import Session as TidalSession +from tidalapi import Track as TidalTrack +from tidalapi import exceptions as tidal_exceptions + +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.helpers.tags import AudioTags, parse_tags +from music_assistant.helpers.throttle_retry import ThrottlerManager, throttle_with_retries +from music_assistant.models.music_provider import MusicProvider from .helpers import ( DEFAULT_LIMIT, @@ -84,13 +77,13 @@ if TYPE_CHECKING: from collections.abc import AsyncGenerator, Awaitable + from music_assistant_models.config_entries import ProviderConfig + from music_assistant_models.provider import ProviderManifest from tidalapi.media import Lyrics as TidalLyrics from tidalapi.media import Stream as TidalStream - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + from music_assistant import MusicAssistant + from music_assistant.models import ProviderInstanceType TOKEN_TYPE = "Bearer" diff --git a/music_assistant/server/providers/tidal/helpers.py b/music_assistant/providers/tidal/helpers.py similarity index 98% rename from music_assistant/server/providers/tidal/helpers.py rename to music_assistant/providers/tidal/helpers.py index f443133f5..9dba4d70a 100644 --- a/music_assistant/server/providers/tidal/helpers.py +++ b/music_assistant/providers/tidal/helpers.py @@ -12,6 +12,8 @@ import asyncio import logging +from music_assistant_models.enums import MediaType +from music_assistant_models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable from tidalapi import Album as TidalAlbum from tidalapi import Artist as TidalArtist from tidalapi import Favorites as TidalFavorites @@ -23,9 +25,6 @@ from tidalapi.exceptions import MetadataNotAvailable, ObjectNotFound, TooManyRequests from tidalapi.media import Stream as TidalStream -from music_assistant.common.models.enums import MediaType -from music_assistant.common.models.errors import MediaNotFoundError, ResourceTemporarilyUnavailable - DEFAULT_LIMIT = 50 LOGGER = logging.getLogger(__name__) diff --git a/music_assistant/server/providers/tidal/icon.svg b/music_assistant/providers/tidal/icon.svg similarity index 100% rename from music_assistant/server/providers/tidal/icon.svg rename to music_assistant/providers/tidal/icon.svg diff --git a/music_assistant/server/providers/tidal/icon_dark.svg b/music_assistant/providers/tidal/icon_dark.svg similarity index 100% rename from music_assistant/server/providers/tidal/icon_dark.svg rename to music_assistant/providers/tidal/icon_dark.svg diff --git a/music_assistant/server/providers/tidal/manifest.json b/music_assistant/providers/tidal/manifest.json similarity index 100% rename from music_assistant/server/providers/tidal/manifest.json rename to music_assistant/providers/tidal/manifest.json diff --git a/music_assistant/server/providers/tunein/__init__.py b/music_assistant/providers/tunein/__init__.py similarity index 93% rename from music_assistant/server/providers/tunein/__init__.py rename to music_assistant/providers/tunein/__init__.py index 55265b1b0..4e605c710 100644 --- a/music_assistant/server/providers/tunein/__init__.py +++ b/music_assistant/providers/tunein/__init__.py @@ -4,10 +4,10 @@ from typing import TYPE_CHECKING -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature, StreamType -from music_assistant.common.models.errors import InvalidDataError, LoginFailed, MediaNotFoundError -from music_assistant.common.models.media_items import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ProviderFeature, StreamType +from music_assistant_models.errors import InvalidDataError, LoginFailed, MediaNotFoundError +from music_assistant_models.media_items import ( AudioFormat, ContentType, ImageType, @@ -16,10 +16,11 @@ ProviderMapping, Radio, ) -from music_assistant.common.models.streamdetails import StreamDetails +from music_assistant_models.streamdetails import StreamDetails + from music_assistant.constants import CONF_USERNAME -from music_assistant.server.helpers.throttle_retry import Throttler -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant.helpers.throttle_retry import Throttler +from music_assistant.models.music_provider import MusicProvider SUPPORTED_FEATURES = ( ProviderFeature.LIBRARY_RADIOS, @@ -29,10 +30,11 @@ if TYPE_CHECKING: from collections.abc import AsyncGenerator - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 async def setup( diff --git a/music_assistant/server/providers/tunein/icon.svg b/music_assistant/providers/tunein/icon.svg similarity index 100% rename from music_assistant/server/providers/tunein/icon.svg rename to music_assistant/providers/tunein/icon.svg diff --git a/music_assistant/server/providers/tunein/manifest.json b/music_assistant/providers/tunein/manifest.json similarity index 100% rename from music_assistant/server/providers/tunein/manifest.json rename to music_assistant/providers/tunein/manifest.json diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/providers/ytmusic/__init__.py similarity index 97% rename from music_assistant/server/providers/ytmusic/__init__.py rename to music_assistant/providers/ytmusic/__init__.py index 34f1f0f87..75a622425 100644 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ b/music_assistant/providers/ytmusic/__init__.py @@ -10,18 +10,15 @@ from urllib.parse import unquote import yt_dlp -from ytmusicapi.constants import SUPPORTED_LANGUAGES -from ytmusicapi.exceptions import YTMusicServerError - -from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType -from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature, StreamType -from music_assistant.common.models.errors import ( +from music_assistant_models.config_entries import ConfigEntry, ConfigValueType +from music_assistant_models.enums import ConfigEntryType, ProviderFeature, StreamType +from music_assistant_models.errors import ( InvalidDataError, LoginFailed, MediaNotFoundError, UnplayableMediaError, ) -from music_assistant.common.models.media_items import ( +from music_assistant_models.media_items import ( Album, AlbumType, Artist, @@ -37,11 +34,11 @@ SearchResults, Track, ) -from music_assistant.common.models.streamdetails import StreamDetails -from music_assistant.server.helpers.auth import AuthenticationHelper -from music_assistant.server.models.music_provider import MusicProvider +from music_assistant_models.streamdetails import StreamDetails +from ytmusicapi.constants import SUPPORTED_LANGUAGES +from ytmusicapi.exceptions import YTMusicServerError -from .helpers import ( +from music_assistant.helpers import ( add_remove_playlist_tracks, get_album, get_artist, @@ -59,12 +56,15 @@ refresh_oauth_token, search, ) +from music_assistant.helpers.auth import AuthenticationHelper +from music_assistant.models.music_provider import MusicProvider if TYPE_CHECKING: - from music_assistant.common.models.config_entries import ProviderConfig - from music_assistant.common.models.provider import ProviderManifest - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderInstanceType + 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 CONF_COOKIE = "cookie" diff --git a/music_assistant/server/providers/ytmusic/helpers.py b/music_assistant/providers/ytmusic/helpers.py similarity index 99% rename from music_assistant/server/providers/ytmusic/helpers.py rename to music_assistant/providers/ytmusic/helpers.py index 6af6dc7d6..f41ac68cf 100644 --- a/music_assistant/server/providers/ytmusic/helpers.py +++ b/music_assistant/providers/ytmusic/helpers.py @@ -20,7 +20,7 @@ OAUTH_USER_AGENT, ) -from music_assistant.server.helpers.auth import AuthenticationHelper +from music_assistant.helpers.auth import AuthenticationHelper async def get_artist( diff --git a/music_assistant/server/providers/ytmusic/icon.svg b/music_assistant/providers/ytmusic/icon.svg similarity index 100% rename from music_assistant/server/providers/ytmusic/icon.svg rename to music_assistant/providers/ytmusic/icon.svg diff --git a/music_assistant/server/providers/ytmusic/manifest.json b/music_assistant/providers/ytmusic/manifest.json similarity index 100% rename from music_assistant/server/providers/ytmusic/manifest.json rename to music_assistant/providers/ytmusic/manifest.json diff --git a/music_assistant/server/__init__.py b/music_assistant/server/__init__.py deleted file mode 100644 index 7fe0caca8..000000000 --- a/music_assistant/server/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Music Assistant: The music library manager in python.""" - -from .server import MusicAssistant # noqa: F401 diff --git a/music_assistant/server/helpers/util.py b/music_assistant/server/helpers/util.py deleted file mode 100644 index 3d342b735..000000000 --- a/music_assistant/server/helpers/util.py +++ /dev/null @@ -1,297 +0,0 @@ -"""Various (server-only) tools and helpers.""" - -from __future__ import annotations - -import asyncio -import functools -import importlib -import logging -import platform -import tempfile -import urllib.error -import urllib.parse -import urllib.request -from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine -from contextlib import suppress -from functools import lru_cache -from importlib.metadata import PackageNotFoundError -from importlib.metadata import version as pkg_version -from types import TracebackType -from typing import TYPE_CHECKING, Any, ParamSpec, Self, TypeVar - -import cchardet as chardet -import ifaddr -import memory_tempfile -from zeroconf import IPVersion - -from music_assistant.server.helpers.process import check_output - -if TYPE_CHECKING: - from collections.abc import Iterator - - from zeroconf.asyncio import AsyncServiceInfo - - from music_assistant.server import MusicAssistant - from music_assistant.server.models import ProviderModuleType - -LOGGER = logging.getLogger(__name__) - -HA_WHEELS = "https://wheels.home-assistant.io/musllinux/" - - -async def install_package(package: str) -> None: - """Install package with pip, raise when install failed.""" - LOGGER.debug("Installing python package %s", package) - args = ["uv", "pip", "install", "--no-cache", "--find-links", HA_WHEELS, package] - return_code, output = await check_output(*args) - - if return_code != 0 and "Permission denied" in output.decode(): - # try again with regular pip - # uv pip seems to have issues with permissions on docker installs - args = [ - "pip", - "install", - "--no-cache-dir", - "--no-input", - "--find-links", - HA_WHEELS, - package, - ] - return_code, output = await check_output(*args) - - if return_code != 0: - msg = f"Failed to install package {package}\n{output.decode()}" - raise RuntimeError(msg) - - -async def get_package_version(pkg_name: str) -> str | None: - """ - Return the version of an installed (python) package. - - Will return None if the package is not found. - """ - try: - return await asyncio.to_thread(pkg_version, pkg_name) - except PackageNotFoundError: - return None - - -async def get_ips(include_ipv6: bool = False, ignore_loopback: bool = True) -> set[str]: - """Return all IP-adresses of all network interfaces.""" - - def call() -> set[str]: - result: set[str] = set() - adapters = ifaddr.get_adapters() - for adapter in adapters: - for ip in adapter.ips: - if ip.is_IPv6 and not include_ipv6: - continue - if ip.ip == "127.0.0.1" and ignore_loopback: - continue - result.add(ip.ip) - return result - - return await asyncio.to_thread(call) - - -async def is_hass_supervisor() -> bool: - """Return if we're running inside the HA Supervisor (e.g. HAOS).""" - - def _check(): - try: - urllib.request.urlopen("http://supervisor/core", timeout=1) - except urllib.error.URLError as err: - # this should return a 401 unauthorized if it exists - return getattr(err, "code", 999) == 401 - except Exception: - return False - return False - - return await asyncio.to_thread(_check) - - -async def load_provider_module(domain: str, requirements: list[str]) -> ProviderModuleType: - """Return module for given provider domain and make sure the requirements are met.""" - - @lru_cache - def _get_provider_module(domain: str) -> ProviderModuleType: - return importlib.import_module(f".{domain}", "music_assistant.server.providers") - - # ensure module requirements are met - for requirement in requirements: - if "==" not in requirement: - # we should really get rid of unpinned requirements - continue - package_name, version = requirement.split("==", 1) - installed_version = await get_package_version(package_name) - if installed_version == "0.0.0": - # ignore editable installs - continue - if installed_version != version: - await install_package(requirement) - - # try to load the module - try: - return await asyncio.to_thread(_get_provider_module, domain) - except ImportError: - # (re)install ALL requirements - for requirement in requirements: - await install_package(requirement) - # try loading the provider again to be safe - # this will fail if something else is wrong (as it should) - return await asyncio.to_thread(_get_provider_module, domain) - - -def create_tempfile(): - """Return a (named) temporary file.""" - # ruff: noqa: SIM115 - if platform.system() == "Linux": - return memory_tempfile.MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0) - return tempfile.NamedTemporaryFile(buffering=0) - - -def divide_chunks(data: bytes, chunk_size: int) -> Iterator[bytes]: - """Chunk bytes data into smaller chunks.""" - for i in range(0, len(data), chunk_size): - yield data[i : i + chunk_size] - - -def get_primary_ip_address_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: - """Get primary IP address from zeroconf discovery info.""" - for address in discovery_info.parsed_addresses(IPVersion.V4Only): - if address.startswith("127"): - # filter out loopback address - continue - if address.startswith("169.254"): - # filter out APIPA address - continue - return address - return None - - -def get_port_from_zeroconf(discovery_info: AsyncServiceInfo) -> str | None: - """Get primary IP address from zeroconf discovery info.""" - return discovery_info.port - - -async def close_async_generator(agen: AsyncGenerator[Any, None]) -> None: - """Force close an async generator.""" - task = asyncio.create_task(agen.__anext__()) - task.cancel() - with suppress(asyncio.CancelledError): - await task - await agen.aclose() - - -async def detect_charset(data: bytes, fallback="utf-8") -> str: - """Detect charset of raw data.""" - try: - detected = await asyncio.to_thread(chardet.detect, data) - if detected and detected["encoding"] and detected["confidence"] > 0.75: - return detected["encoding"] - except Exception as err: - LOGGER.debug("Failed to detect charset: %s", err) - return fallback - - -class TaskManager: - """ - Helper class to run many tasks at once. - - This is basically an alternative to asyncio.TaskGroup but this will not - cancel all operations when one of the tasks fails. - Logging of exceptions is done by the mass.create_task helper. - """ - - def __init__(self, mass: MusicAssistant, limit: int = 0): - """Initialize the TaskManager.""" - self.mass = mass - self._tasks: list[asyncio.Task] = [] - self._semaphore = asyncio.Semaphore(limit) if limit else None - - def create_task(self, coro: Coroutine) -> asyncio.Task: - """Create a new task and add it to the manager.""" - task = self.mass.create_task(coro) - self._tasks.append(task) - return task - - async def create_task_with_limit(self, coro: Coroutine) -> None: - """Create a new task with semaphore limit.""" - assert self._semaphore is not None - - def task_done_callback(_task: asyncio.Task) -> None: - self._tasks.remove(task) - self._semaphore.release() - - await self._semaphore.acquire() - task: asyncio.Task = self.create_task(coro) - task.add_done_callback(task_done_callback) - - async def __aenter__(self) -> Self: - """Enter context manager.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit context manager.""" - if len(self._tasks) > 0: - await asyncio.wait(self._tasks) - self._tasks.clear() - - -_R = TypeVar("_R") -_P = ParamSpec("_P") - - -def lock( - func: Callable[_P, Awaitable[_R]], -) -> Callable[_P, Coroutine[Any, Any, _R]]: - """Call async function using a Lock.""" - - @functools.wraps(func) - async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: - """Call async function using the throttler with retries.""" - if not (func_lock := getattr(func, "lock", None)): - func_lock = asyncio.Lock() - func.lock = func_lock - async with func_lock: - return await func(*args, **kwargs) - - return wrapper - - -class TimedAsyncGenerator: - """ - Async iterable that times out after a given time. - - Source: https://medium.com/@dmitry8912/implementing-timeouts-in-pythons-asynchronous-generators-f7cbaa6dc1e9 - """ - - def __init__(self, iterable, timeout=0): - """ - Initialize the AsyncTimedIterable. - - Args: - iterable: The async iterable to wrap. - timeout: The timeout in seconds for each iteration. - """ - - class AsyncTimedIterator: - def __init__(self): - self._iterator = iterable.__aiter__() - - async def __anext__(self): - result = await asyncio.wait_for(self._iterator.__anext__(), int(timeout)) - if not result: - raise StopAsyncIteration - return result - - self._factory = AsyncTimedIterator - - def __aiter__(self): - """Return the async iterator.""" - return self._factory() diff --git a/mypy.ini b/mypy.ini index b2bdf56c7..aa3c6247b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -21,4 +21,4 @@ disallow_untyped_decorators = true disallow_untyped_defs = true warn_return_any = true warn_unreachable = true -packages=tests,music_assistant.client,music_assistant.common,music_assistant.server.providers.builtin,music_assistant.server.providers.filesystem_local,music_assistant.server.providers.filesystem_smb,music_assistant.server.providers.fully_kiosk,music_assistant.server.providers.jellyfin,music_assistant.server.providers.plex,music_assistant.server.providers.radiobrowser,music_assistant.server.providers.test,music_assistant.server.providers.theaudiodb,music_assistant.server.providers.tidal +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 3fb50c926..9d841ba5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,38 +9,38 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -dependencies = ["aiohttp", "orjson", "mashumaro"] -description = "Music Assistant" -license = {text = "Apache-2.0"} -readme = "README.md" -requires-python = ">=3.11" -version = "0.0.0" - -[project.optional-dependencies] -server = [ - "faust-cchardet>=2.1.18", +dependencies = [ "aiodns>=3.0.0", "Brotli>=1.0.9", "aiohttp==3.10.10", "aiofiles==24.1.0", "aiorun==2024.8.1", + "aiosqlite==0.20.0", "certifi==2024.8.30", "colorlog==6.8.2", - "aiosqlite==0.20.0", + "cryptography==43.0.3", "eyeD3==0.9.7", - "python-slugify==8.0.4", + "faust-cchardet>=2.1.18", + "ifaddr==0.2.0", "mashumaro==3.14", "memory-tempfile==2.2.3", "music-assistant-frontend==v2.9.14", + "music-assistant-models==1.0.3", + "orjson==3.10.7", "pillow==11.0.0", + "python-slugify==8.0.4", "unidecode==1.3.8", "xmltodict==0.14.2", - "orjson==3.10.10", "shortuuid==1.0.13", - "zeroconf==0.136.0", - "cryptography==43.0.3", - "ifaddr==0.2.0", + "zeroconf==0.135.0", ] +description = "Music Assistant" +license = {text = "Apache-2.0"} +readme = "README.md" +requires-python = ">=3.11" +version = "0.0.0" + +[project.optional-dependencies] test = [ "codespell==2.3.0", "isort==5.13.2", diff --git a/requirements_all.txt b/requirements_all.txt index ecb2fd6b4..1ebea1f57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,7 +23,8 @@ ifaddr==0.2.0 mashumaro==3.14 memory-tempfile==2.2.3 music-assistant-frontend==v2.9.14 -orjson==3.10.10 +music-assistant-models==1.0.3 +orjson==3.10.7 pillow==11.0.0 pkce==1.0.3 plexapi==4.15.16 @@ -46,4 +47,4 @@ xmltodict==0.14.2 yt-dlp==2024.10.7 yt-dlp-youtube-accesstoken==0.1.1 ytmusicapi==1.8.1 -zeroconf==0.136.0 +zeroconf==0.135.0 diff --git a/scripts/gen_requirements_all.py b/scripts/gen_requirements_all.py index f10f45815..cff09664a 100644 --- a/scripts/gen_requirements_all.py +++ b/scripts/gen_requirements_all.py @@ -19,17 +19,13 @@ def gather_core_requirements() -> list[str]: """Gather core requirements out of pyproject.toml.""" with open("pyproject.toml", "rb") as fp: data = tomllib.load(fp) - # server deps - dependencies: list[str] = data["project"]["optional-dependencies"]["server"] - # regular/client deps - dependencies += data["project"]["dependencies"] - return dependencies + return data["project"]["dependencies"] def gather_requirements_from_manifests() -> list[str]: """Gather all of the requirements from provider manifests.""" dependencies: list[str] = [] - providers_path = "music_assistant/server/providers" + providers_path = "music_assistant/providers" for dir_str in os.listdir(providers_path): dir_path = os.path.join(providers_path, dir_str) if not os.path.isdir(dir_path): diff --git a/scripts/setup.sh b/scripts/setup.sh index 53f161a18..1cff7a174 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -21,6 +21,6 @@ echo "Installing development dependencies..." pip install --upgrade pip pip install --upgrade uv -uv pip install -e ".[server]" +uv pip install -e "." uv pip install -e ".[test]" pre-commit install diff --git a/tests/__init__.py b/tests/__init__.py index 0a066f8c4..326fb7be2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Tests for Music Assistant go here.""" +"""Tests for the Music Assistant server.""" diff --git a/tests/common.py b/tests/common.py index 394540b21..a6ee1005b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -6,10 +6,10 @@ from collections.abc import AsyncGenerator import aiofiles.os +from music_assistant_models.enums import EventType +from music_assistant_models.event import MassEvent -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.event import MassEvent -from music_assistant.server.server import MusicAssistant +from music_assistant import MusicAssistant def _get_fixture_folder(provider: str | None = None) -> pathlib.Path: diff --git a/tests/conftest.py b/tests/conftest.py index fc25b0969..62c294cd3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest -from music_assistant.server.server import MusicAssistant +from music_assistant import MusicAssistant from tests.common import wait_for_sync_completion diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 000000000..c062c1d44 --- /dev/null +++ b/tests/core/__init__.py @@ -0,0 +1 @@ +"""Tests for the Music Assistant server core logic.""" diff --git a/tests/server/test_compare.py b/tests/core/test_compare.py similarity index 99% rename from tests/server/test_compare.py rename to tests/core/test_compare.py index ff9eaad0e..8c25f7abe 100644 --- a/tests/server/test_compare.py +++ b/tests/core/test_compare.py @@ -1,7 +1,8 @@ """Tests for mediaitem compare helper functions.""" -from music_assistant.common.models import media_items -from music_assistant.server.helpers import compare +from music_assistant_models import media_items + +from music_assistant.helpers import compare def test_compare_version() -> None: diff --git a/tests/test_helpers.py b/tests/core/test_helpers.py similarity index 91% rename from tests/test_helpers.py rename to tests/core/test_helpers.py index a01934e5e..b77fdd5ab 100644 --- a/tests/test_helpers.py +++ b/tests/core/test_helpers.py @@ -1,11 +1,10 @@ """Tests for utility/helper functions.""" import pytest +from music_assistant_models import media_items +from music_assistant_models.errors import MusicAssistantError -from music_assistant.common.helpers import uri, util -from music_assistant.common.models import media_items -from music_assistant.common.models.errors import MusicAssistantError -from music_assistant.constants import SILENCE_FILE +from music_assistant.helpers import uri, util def test_version_extract() -> None: @@ -73,11 +72,11 @@ async def test_uri_parsing() -> None: assert provider == "builtin" assert item_id == "http://radiostream.io/stream.mp3" # test local file to builtin provider - test_uri = SILENCE_FILE + test_uri = __file__ media_type, provider, item_id = await uri.parse_uri(test_uri) assert media_type == media_items.MediaType.UNKNOWN assert provider == "builtin" - assert item_id == SILENCE_FILE + assert item_id == __file__ # test invalid uri with pytest.raises(MusicAssistantError): await uri.parse_uri("invalid://blah") diff --git a/tests/test_radio_stream_title.py b/tests/core/test_radio_stream_title.py similarity index 97% rename from tests/test_radio_stream_title.py rename to tests/core/test_radio_stream_title.py index 8dbb5f9d4..b76d91227 100644 --- a/tests/test_radio_stream_title.py +++ b/tests/core/test_radio_stream_title.py @@ -1,6 +1,6 @@ """Tests for cleaning radio streamtitle.""" -from music_assistant.common.helpers.util import clean_stream_title +from music_assistant.helpers.util import clean_stream_title def test_cleaning_streamtitle() -> None: diff --git a/tests/server/test_server.py b/tests/core/test_server_base.py similarity index 89% rename from tests/server/test_server.py rename to tests/core/test_server_base.py index ee1b58b81..d40867872 100644 --- a/tests/server/test_server.py +++ b/tests/core/test_server_base.py @@ -2,9 +2,10 @@ import asyncio -from music_assistant.common.models.enums import EventType -from music_assistant.common.models.event import MassEvent -from music_assistant.server.server import MusicAssistant +from music_assistant_models.enums import EventType +from music_assistant_models.event import MassEvent + +from music_assistant import MusicAssistant async def test_start_and_stop_server(mass: MusicAssistant) -> None: diff --git a/tests/test_tags.py b/tests/core/test_tags.py similarity index 98% rename from tests/test_tags.py rename to tests/core/test_tags.py index be9a94f9d..fea52e30b 100644 --- a/tests/test_tags.py +++ b/tests/core/test_tags.py @@ -2,7 +2,7 @@ import pathlib -from music_assistant.server.helpers import tags +from music_assistant.helpers import tags RESOURCES_DIR = pathlib.Path(__file__).parent.resolve().joinpath("fixtures") diff --git a/tests/server/providers/filesystem/__init__.py b/tests/providers/filesystem/__init__.py similarity index 100% rename from tests/server/providers/filesystem/__init__.py rename to tests/providers/filesystem/__init__.py diff --git a/tests/server/providers/filesystem/test_helpers.py b/tests/providers/filesystem/test_helpers.py similarity index 98% rename from tests/server/providers/filesystem/test_helpers.py rename to tests/providers/filesystem/test_helpers.py index cac1bb781..591a36ec5 100644 --- a/tests/server/providers/filesystem/test_helpers.py +++ b/tests/providers/filesystem/test_helpers.py @@ -2,7 +2,7 @@ import pytest -from music_assistant.server.providers.filesystem_local import helpers +from music_assistant.providers.filesystem_local import helpers # ruff: noqa: S108 diff --git a/tests/server/providers/jellyfin/__init__.py b/tests/providers/jellyfin/__init__.py similarity index 100% rename from tests/server/providers/jellyfin/__init__.py rename to tests/providers/jellyfin/__init__.py diff --git a/tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr b/tests/providers/jellyfin/__snapshots__/test_parsers.ambr similarity index 100% rename from tests/server/providers/jellyfin/__snapshots__/test_parsers.ambr rename to tests/providers/jellyfin/__snapshots__/test_parsers.ambr diff --git a/tests/server/providers/jellyfin/fixtures/albums/infest.json b/tests/providers/jellyfin/fixtures/albums/infest.json similarity index 100% rename from tests/server/providers/jellyfin/fixtures/albums/infest.json rename to tests/providers/jellyfin/fixtures/albums/infest.json diff --git a/tests/server/providers/jellyfin/fixtures/albums/this_is_christmas.json b/tests/providers/jellyfin/fixtures/albums/this_is_christmas.json similarity index 100% rename from tests/server/providers/jellyfin/fixtures/albums/this_is_christmas.json rename to tests/providers/jellyfin/fixtures/albums/this_is_christmas.json diff --git a/tests/server/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json b/tests/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json similarity index 100% rename from tests/server/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json rename to tests/providers/jellyfin/fixtures/albums/yesterday_when_i_was_mad.json diff --git a/tests/server/providers/jellyfin/fixtures/artists/ash.json b/tests/providers/jellyfin/fixtures/artists/ash.json similarity index 100% rename from tests/server/providers/jellyfin/fixtures/artists/ash.json rename to tests/providers/jellyfin/fixtures/artists/ash.json diff --git a/tests/server/providers/jellyfin/fixtures/tracks/thrown_away.json b/tests/providers/jellyfin/fixtures/tracks/thrown_away.json similarity index 100% rename from tests/server/providers/jellyfin/fixtures/tracks/thrown_away.json rename to tests/providers/jellyfin/fixtures/tracks/thrown_away.json diff --git a/tests/server/providers/jellyfin/fixtures/tracks/where_the_bands_are.json b/tests/providers/jellyfin/fixtures/tracks/where_the_bands_are.json similarity index 100% rename from tests/server/providers/jellyfin/fixtures/tracks/where_the_bands_are.json rename to tests/providers/jellyfin/fixtures/tracks/where_the_bands_are.json diff --git a/tests/server/providers/jellyfin/fixtures/tracks/zombie_christmas.json b/tests/providers/jellyfin/fixtures/tracks/zombie_christmas.json similarity index 100% rename from tests/server/providers/jellyfin/fixtures/tracks/zombie_christmas.json rename to tests/providers/jellyfin/fixtures/tracks/zombie_christmas.json diff --git a/tests/server/providers/jellyfin/test_init.py b/tests/providers/jellyfin/test_init.py similarity index 87% rename from tests/server/providers/jellyfin/test_init.py rename to tests/providers/jellyfin/test_init.py index 1222ef583..44e04eae5 100644 --- a/tests/server/providers/jellyfin/test_init.py +++ b/tests/providers/jellyfin/test_init.py @@ -5,9 +5,9 @@ import pytest from aiojellyfin.testing import FixtureBuilder +from music_assistant_models.config_entries import ProviderConfig -from music_assistant.common.models.config_entries import ProviderConfig -from music_assistant.server.server import MusicAssistant +from music_assistant import MusicAssistant from tests.common import get_fixtures_dir, wait_for_sync_completion @@ -26,9 +26,7 @@ async def jellyfin_provider(mass: MusicAssistant) -> AsyncGenerator[ProviderConf authenticate_by_name = f.to_authenticate_by_name() - with mock.patch( - "music_assistant.server.providers.jellyfin.authenticate_by_name", authenticate_by_name - ): + with mock.patch(".providers.jellyfin.authenticate_by_name", authenticate_by_name): async with wait_for_sync_completion(mass): config = await mass.config.save_provider_config( "jellyfin", diff --git a/tests/server/providers/jellyfin/test_parsers.py b/tests/providers/jellyfin/test_parsers.py similarity index 96% rename from tests/server/providers/jellyfin/test_parsers.py rename to tests/providers/jellyfin/test_parsers.py index 12a49457e..7952cae51 100644 --- a/tests/server/providers/jellyfin/test_parsers.py +++ b/tests/providers/jellyfin/test_parsers.py @@ -11,7 +11,7 @@ from mashumaro.codecs.json import JSONDecoder from syrupy.assertion import SnapshotAssertion -from music_assistant.server.providers.jellyfin.parsers import parse_album, parse_artist, parse_track +from music_assistant.providers.jellyfin.parsers import parse_album, parse_artist, parse_track FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" ARTIST_FIXTURES = list(FIXTURES_DIR.glob("artists/*.json")) diff --git a/tests/server/__init__.py b/tests/server/__init__.py deleted file mode 100644 index 326fb7be2..000000000 --- a/tests/server/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Music Assistant server."""