Merge branch '189-use-supported-schemas'
This commit is contained in:
@@ -19,6 +19,8 @@ v0.11.1
|
|||||||
* Fixed issue where users couldn't log in to LMS due to Sublime Music always
|
* Fixed issue where users couldn't log in to LMS due to Sublime Music always
|
||||||
sending version number "1.15.0" instead of figuring out what version of the
|
sending version number "1.15.0" instead of figuring out what version of the
|
||||||
API the server actually reports.
|
API the server actually reports.
|
||||||
|
* Fixed issue where edits to the music provider configurations were applied even
|
||||||
|
if ESC was pressed. (#247)
|
||||||
|
|
||||||
v0.11.0
|
v0.11.0
|
||||||
=======
|
=======
|
||||||
|
@@ -438,7 +438,6 @@ class Adapter(abc.ABC):
|
|||||||
Examples of values that could be provided include ``http``, ``https``, ``file``,
|
Examples of values that could be provided include ``http``, ``https``, ``file``,
|
||||||
or ``ftp``.
|
or ``ftp``.
|
||||||
"""
|
"""
|
||||||
# TODO (#189) actually use this
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -446,18 +445,19 @@ class Adapter(abc.ABC):
|
|||||||
"""
|
"""
|
||||||
Whether or not the adapter supports :class:`get_cover_art_uri`.
|
Whether or not the adapter supports :class:`get_cover_art_uri`.
|
||||||
"""
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_stream(self) -> bool:
|
def can_get_song_file_uri(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Whether or not the adapter can provide a stream URI.
|
Whether or not the adapter supports :class:`get_song_file_uri`.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_get_song_uri(self) -> bool:
|
def can_get_song_stream_uri(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Whether or not the adapter supports :class:`get_song_uri`.
|
Whether or not the adapter supports :class:`get_song_stream_uri`.
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -645,20 +645,26 @@ class Adapter(abc.ABC):
|
|||||||
"""
|
"""
|
||||||
raise self._check_can_error("get_cover_art_uri")
|
raise self._check_can_error("get_cover_art_uri")
|
||||||
|
|
||||||
def get_song_uri(self, song_id: str, scheme: str, stream: bool = False) -> str:
|
def get_song_file_uri(self, song_id: str, schemes: Iterable[str]) -> str:
|
||||||
"""
|
"""
|
||||||
Get a URI for a given song.
|
Get a URI for a given song. This URI must give the full file.
|
||||||
|
|
||||||
:param song_id: The ID of the song to get a URI for.
|
:param song_id: The ID of the song to get a URI for.
|
||||||
:param scheme: The URI scheme that should be returned. It is guaranteed that
|
:param schemes: A set of URI schemes that can be returned. It is guaranteed that
|
||||||
``scheme`` will be one of the schemes returned by
|
all of the items in ``schemes`` will be one of the schemes returned by
|
||||||
:class:`supported_schemes`.
|
:class:`supported_schemes`.
|
||||||
:param stream: Whether or not the URI returned should be a stream URI. This will
|
|
||||||
only be ``True`` if :class:`supports_streaming` returns ``True``.
|
|
||||||
:returns: The URI for the given song.
|
:returns: The URI for the given song.
|
||||||
"""
|
"""
|
||||||
# TODO (#189)
|
raise self._check_can_error("get_song_file_uri")
|
||||||
raise self._check_can_error("get_song_uri")
|
|
||||||
|
def get_song_stream_uri(self, song_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Get a URI for streaming the given song.
|
||||||
|
|
||||||
|
:param song_id: The ID of the song to get the stream URI for.
|
||||||
|
:returns: the stream URI for the given song.
|
||||||
|
"""
|
||||||
|
raise self._check_can_error("get_song_stream_uri")
|
||||||
|
|
||||||
def get_song_details(self, song_id: str) -> Song:
|
def get_song_details(self, song_id: str) -> Song:
|
||||||
"""
|
"""
|
||||||
|
@@ -101,7 +101,7 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
|
|
||||||
# TODO (#200) make these dependent on cache state. Need to do this kinda efficiently
|
# TODO (#200) make these dependent on cache state. Need to do this kinda efficiently
|
||||||
can_get_cover_art_uri = True
|
can_get_cover_art_uri = True
|
||||||
can_get_song_uri = True
|
can_get_song_file_uri = True
|
||||||
can_get_song_details = True
|
can_get_song_details = True
|
||||||
can_get_artist = True
|
can_get_artist = True
|
||||||
can_get_albums = True
|
can_get_albums = True
|
||||||
@@ -286,7 +286,7 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
|
|
||||||
raise CacheMissError()
|
raise CacheMissError()
|
||||||
|
|
||||||
def get_song_uri(self, song_id: str, scheme: str, stream: bool = False) -> str:
|
def get_song_file_uri(self, song_id: str, schemes: Iterable[str]) -> str:
|
||||||
song = models.Song.get_or_none(models.Song.id == song_id)
|
song = models.Song.get_or_none(models.Song.id == song_id)
|
||||||
if not song:
|
if not song:
|
||||||
if self.is_cache:
|
if self.is_cache:
|
||||||
|
@@ -539,17 +539,6 @@ class AdapterManager:
|
|||||||
|
|
||||||
return future_finished
|
return future_finished
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_scheme() -> str:
|
|
||||||
# TODO (#189): eventually this will come from the players
|
|
||||||
assert AdapterManager._instance
|
|
||||||
scheme_priority = ("https", "http")
|
|
||||||
schemes = sorted(
|
|
||||||
AdapterManager._instance.ground_truth_adapter.supported_schemes,
|
|
||||||
key=scheme_priority.index,
|
|
||||||
)
|
|
||||||
return list(schemes)[0]
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_supported_artist_query_types() -> Set[AlbumSearchQuery.Type]:
|
def get_supported_artist_query_types() -> Set[AlbumSearchQuery.Type]:
|
||||||
assert AdapterManager._instance
|
assert AdapterManager._instance
|
||||||
@@ -666,13 +655,17 @@ class AdapterManager:
|
|||||||
return AdapterManager._ground_truth_can_do("delete_playlist")
|
return AdapterManager._ground_truth_can_do("delete_playlist")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_get_song_filename_or_stream() -> bool:
|
def can_get_song_file_uri() -> bool:
|
||||||
return AdapterManager._ground_truth_can_do("get_song_uri")
|
return AdapterManager._ground_truth_can_do("get_song_file_uri")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def can_get_song_stream_uri() -> bool:
|
||||||
|
return AdapterManager._ground_truth_can_do("get_song_stream_uri")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_batch_download_songs() -> bool:
|
def can_batch_download_songs() -> bool:
|
||||||
# We can only download from the ground truth adapter.
|
# We can only download from the ground truth adapter.
|
||||||
return AdapterManager._ground_truth_can_do("get_song_uri")
|
return AdapterManager._ground_truth_can_do("get_song_file_uri")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def can_get_genres() -> bool:
|
def can_get_genres() -> bool:
|
||||||
@@ -805,122 +798,133 @@ class AdapterManager:
|
|||||||
CachingAdapter.CachedDataKey.PLAYLIST_DETAILS, playlist_id
|
CachingAdapter.CachedDataKey.PLAYLIST_DETAILS, playlist_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO (#189): allow this to take a set of schemes and unify with
|
|
||||||
# get_cover_art_filename
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_cover_art_uri(cover_art_id: str = None, size: int = 300) -> str:
|
def _get_networked_scheme() -> str:
|
||||||
assert AdapterManager._instance
|
assert AdapterManager._instance
|
||||||
|
networked_scheme_priority = ("https", "http")
|
||||||
|
return sorted(
|
||||||
|
AdapterManager._instance.ground_truth_adapter.supported_schemes,
|
||||||
|
key=lambda s: networked_scheme_priority.index(s),
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_cover_art_uri(
|
||||||
|
cover_art_id: Optional[str],
|
||||||
|
scheme: str,
|
||||||
|
size: int = 300,
|
||||||
|
before_download: Callable[[], None] = None,
|
||||||
|
force: bool = False,
|
||||||
|
allow_download: bool = True,
|
||||||
|
) -> Result[str]:
|
||||||
|
existing_filename = str(
|
||||||
|
Path(__file__).parent.joinpath("images/default-album-art.png")
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
not AdapterManager._ground_truth_can_do("get_cover_art_uri")
|
not AdapterManager._ground_truth_can_do("get_cover_art_uri")
|
||||||
or not cover_art_id
|
or not cover_art_id
|
||||||
):
|
):
|
||||||
return ""
|
return Result(existing_filename if scheme == "file" else "")
|
||||||
|
|
||||||
return AdapterManager._instance.ground_truth_adapter.get_cover_art_uri(
|
|
||||||
cover_art_id, AdapterManager._get_scheme(), size=size
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_cover_art_filename(
|
|
||||||
cover_art_id: str = None,
|
|
||||||
before_download: Callable[[], None] = None,
|
|
||||||
force: bool = False, # TODO (#202): rename to use_ground_truth_adapter?
|
|
||||||
allow_download: bool = True,
|
|
||||||
) -> Result[str]:
|
|
||||||
existing_cover_art_filename = str(
|
|
||||||
Path(__file__).parent.joinpath("images/default-album-art.png")
|
|
||||||
)
|
|
||||||
if cover_art_id is None:
|
|
||||||
return Result(existing_cover_art_filename)
|
|
||||||
|
|
||||||
assert AdapterManager._instance
|
assert AdapterManager._instance
|
||||||
|
supported_schemes = (
|
||||||
# If the ground truth adapter can't provide cover art, just give up immediately.
|
AdapterManager._instance.ground_truth_adapter.supported_schemes
|
||||||
if not AdapterManager._ground_truth_can_do("get_cover_art_uri"):
|
|
||||||
return Result(existing_cover_art_filename)
|
|
||||||
|
|
||||||
# There could be partial data if the cover art exists, but for some reason was
|
|
||||||
# marked out-of-date.
|
|
||||||
if AdapterManager._can_use_cache(force, "get_cover_art_uri"):
|
|
||||||
assert AdapterManager._instance.caching_adapter
|
|
||||||
try:
|
|
||||||
return Result(
|
|
||||||
AdapterManager._instance.caching_adapter.get_cover_art_uri(
|
|
||||||
cover_art_id, "file", size=300
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except CacheMissError as e:
|
|
||||||
if e.partial_data is not None:
|
|
||||||
existing_cover_art_filename = cast(str, e.partial_data)
|
|
||||||
logging.info(f'Cache Miss on {"get_cover_art_uri"}.')
|
|
||||||
except Exception:
|
|
||||||
logging.exception(
|
|
||||||
f'Error on {"get_cover_art_uri"} retrieving from cache.'
|
|
||||||
)
|
|
||||||
|
|
||||||
if AdapterManager._instance.caching_adapter and force:
|
|
||||||
AdapterManager._instance.caching_adapter.invalidate_data(
|
|
||||||
CachingAdapter.CachedDataKey.COVER_ART_FILE, cover_art_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if not allow_download or (
|
|
||||||
AdapterManager._offline_mode
|
|
||||||
and AdapterManager._instance.ground_truth_adapter.is_networked
|
|
||||||
):
|
|
||||||
return Result(existing_cover_art_filename)
|
|
||||||
|
|
||||||
future: Result[str] = AdapterManager._create_download_result(
|
|
||||||
AdapterManager._instance.ground_truth_adapter.get_cover_art_uri(
|
|
||||||
cover_art_id, AdapterManager._get_scheme(), size=300
|
|
||||||
),
|
|
||||||
cover_art_id,
|
|
||||||
before_download,
|
|
||||||
default_value=existing_cover_art_filename,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if AdapterManager._instance.caching_adapter:
|
# If the scheme is supported natively, then return it.
|
||||||
future.add_done_callback(
|
if scheme in supported_schemes:
|
||||||
AdapterManager._create_caching_done_callback(
|
uri = AdapterManager._instance.ground_truth_adapter.get_cover_art_uri(
|
||||||
|
cover_art_id, scheme, size=size
|
||||||
|
)
|
||||||
|
return Result(uri)
|
||||||
|
|
||||||
|
# If the scheme is "file", then we may need to try to download.
|
||||||
|
if scheme == "file" and (
|
||||||
|
"http" in supported_schemes or "https" in supported_schemes
|
||||||
|
):
|
||||||
|
if AdapterManager._can_use_cache(force, "get_cover_art_uri"):
|
||||||
|
assert AdapterManager._instance.caching_adapter
|
||||||
|
try:
|
||||||
|
return Result(
|
||||||
|
AdapterManager._instance.caching_adapter.get_cover_art_uri(
|
||||||
|
cover_art_id, "file", size=size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except CacheMissError as e:
|
||||||
|
if e.partial_data is not None:
|
||||||
|
existing_filename = cast(str, e.partial_data)
|
||||||
|
logging.info("Cache Miss on get_cover_art_uri.")
|
||||||
|
except Exception:
|
||||||
|
logging.exception(
|
||||||
|
"Error on get_cover_art_uri retrieving from cache."
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we are forcing, invalidate the existing cached data.
|
||||||
|
if AdapterManager._instance.caching_adapter and force:
|
||||||
|
AdapterManager._instance.caching_adapter.invalidate_data(
|
||||||
CachingAdapter.CachedDataKey.COVER_ART_FILE, cover_art_id
|
CachingAdapter.CachedDataKey.COVER_ART_FILE, cover_art_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not allow_download or (
|
||||||
|
AdapterManager._offline_mode
|
||||||
|
and AdapterManager._instance.ground_truth_adapter.is_networked
|
||||||
|
):
|
||||||
|
return Result(existing_filename)
|
||||||
|
|
||||||
|
# Create a download result.
|
||||||
|
future = AdapterManager._create_download_result(
|
||||||
|
AdapterManager._instance.ground_truth_adapter.get_cover_art_uri(
|
||||||
|
cover_art_id, AdapterManager._get_networked_scheme(), size=size,
|
||||||
|
),
|
||||||
|
cover_art_id,
|
||||||
|
before_download,
|
||||||
|
default_value=existing_filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
return future
|
if AdapterManager._instance.caching_adapter:
|
||||||
|
future.add_done_callback(
|
||||||
|
AdapterManager._create_caching_done_callback(
|
||||||
|
CachingAdapter.CachedDataKey.COVER_ART_FILE, cover_art_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return future
|
||||||
|
|
||||||
|
return Result("")
|
||||||
|
|
||||||
# TODO (#189): allow this to take a set of schemes
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_song_filename_or_stream(
|
def get_song_file_uri(song: Song) -> str:
|
||||||
song: Song, format: str = None, force_stream: bool = False
|
|
||||||
) -> str:
|
|
||||||
assert AdapterManager._instance
|
assert AdapterManager._instance
|
||||||
cached_song_filename = None
|
cached_song_filename = None
|
||||||
if AdapterManager._can_use_cache(force_stream, "get_song_uri"):
|
if AdapterManager._can_use_cache(False, "get_song_file_uri"):
|
||||||
assert AdapterManager._instance.caching_adapter
|
assert (caching_adapter := AdapterManager._instance.caching_adapter)
|
||||||
try:
|
try:
|
||||||
return AdapterManager._instance.caching_adapter.get_song_uri(
|
if "file" not in caching_adapter.supported_schemes:
|
||||||
song.id, "file"
|
raise Exception("file not a supported scheme")
|
||||||
)
|
|
||||||
|
return caching_adapter.get_song_file_uri(song.id, "file")
|
||||||
except CacheMissError as e:
|
except CacheMissError as e:
|
||||||
if e.partial_data is not None:
|
if e.partial_data is not None:
|
||||||
cached_song_filename = cast(str, e.partial_data)
|
cached_song_filename = cast(str, e.partial_data)
|
||||||
logging.info(f'Cache Miss on {"get_song_filename_or_stream"}.')
|
logging.info("Cache Miss on get_song_file_uri.")
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(
|
logging.exception("Error on get_song_file_uri retrieving from cache.")
|
||||||
f'Error on {"get_song_filename_or_stream"} retrieving from cache.'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
ground_truth_adapter = AdapterManager._instance.ground_truth_adapter
|
||||||
if (
|
if (
|
||||||
not AdapterManager._ground_truth_can_do("stream")
|
not AdapterManager._ground_truth_can_do("get_song_file_uri")
|
||||||
or not AdapterManager._ground_truth_can_do("get_song_uri")
|
or (ground_truth_adapter.is_networked and AdapterManager._offline_mode)
|
||||||
or (
|
or ("file" not in ground_truth_adapter.supported_schemes)
|
||||||
AdapterManager._instance.ground_truth_adapter.is_networked
|
|
||||||
and AdapterManager._offline_mode
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
raise CacheMissError(partial_data=cached_song_filename)
|
raise CacheMissError(partial_data=cached_song_filename)
|
||||||
|
|
||||||
return AdapterManager._instance.ground_truth_adapter.get_song_uri(
|
return ground_truth_adapter.get_song_file_uri(song.id, "file")
|
||||||
song.id, AdapterManager._get_scheme(), stream=True,
|
|
||||||
|
@staticmethod
|
||||||
|
def get_song_stream_uri(song: Song) -> str:
|
||||||
|
assert AdapterManager._instance
|
||||||
|
# TODO
|
||||||
|
return AdapterManager._instance.ground_truth_adapter.get_song_stream_uri(
|
||||||
|
song.id
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -968,7 +972,9 @@ class AdapterManager:
|
|||||||
# Download the actual song file.
|
# Download the actual song file.
|
||||||
try:
|
try:
|
||||||
# If the song file is already cached, just indicate done immediately.
|
# If the song file is already cached, just indicate done immediately.
|
||||||
AdapterManager._instance.caching_adapter.get_song_uri(song_id, "file")
|
AdapterManager._instance.caching_adapter.get_song_file_uri(
|
||||||
|
song_id, "file"
|
||||||
|
)
|
||||||
AdapterManager._instance.download_limiter_semaphore.release()
|
AdapterManager._instance.download_limiter_semaphore.release()
|
||||||
AdapterManager._instance.song_download_progress(
|
AdapterManager._instance.song_download_progress(
|
||||||
song_id, DownloadProgress(DownloadProgress.Type.DONE),
|
song_id, DownloadProgress(DownloadProgress.Type.DONE),
|
||||||
@@ -984,8 +990,8 @@ class AdapterManager:
|
|||||||
song_tmp_filename_result: Result[
|
song_tmp_filename_result: Result[
|
||||||
str
|
str
|
||||||
] = AdapterManager._create_download_result(
|
] = AdapterManager._create_download_result(
|
||||||
AdapterManager._instance.ground_truth_adapter.get_song_uri(
|
AdapterManager._instance.ground_truth_adapter.get_song_file_uri(
|
||||||
song_id, AdapterManager._get_scheme()
|
song_id, AdapterManager._get_networked_scheme()
|
||||||
),
|
),
|
||||||
song_id,
|
song_id,
|
||||||
lambda: before_download(song_id),
|
lambda: before_download(song_id),
|
||||||
|
@@ -281,7 +281,8 @@ class SubsonicAdapter(Adapter):
|
|||||||
can_get_playlist_details = True
|
can_get_playlist_details = True
|
||||||
can_get_playlists = True
|
can_get_playlists = True
|
||||||
can_get_song_details = True
|
can_get_song_details = True
|
||||||
can_get_song_uri = True
|
can_get_song_file_uri = True
|
||||||
|
can_get_song_stream_uri = True
|
||||||
can_scrobble_song = True
|
can_scrobble_song = True
|
||||||
can_search = True
|
can_search = True
|
||||||
can_stream = True
|
can_stream = True
|
||||||
@@ -537,10 +538,14 @@ class SubsonicAdapter(Adapter):
|
|||||||
params = {"id": cover_art_id, "size": size, **self._get_params()}
|
params = {"id": cover_art_id, "size": size, **self._get_params()}
|
||||||
return self._make_url("getCoverArt") + "?" + urlencode(params)
|
return self._make_url("getCoverArt") + "?" + urlencode(params)
|
||||||
|
|
||||||
def get_song_uri(self, song_id: str, scheme: str, stream: bool = False) -> str:
|
def get_song_file_uri(self, song_id: str, schemes: Iterable[str]) -> str:
|
||||||
|
assert any(s in schemes for s in self.supported_schemes)
|
||||||
params = {"id": song_id, **self._get_params()}
|
params = {"id": song_id, **self._get_params()}
|
||||||
endpoint = "stream" if stream else "download"
|
return self._make_url("download") + "?" + urlencode(params)
|
||||||
return self._make_url(endpoint) + "?" + urlencode(params)
|
|
||||||
|
def get_song_stream_uri(self, song_id: str) -> str:
|
||||||
|
params = {"id": song_id, **self._get_params()}
|
||||||
|
return self._make_url("stream") + "?" + urlencode(params)
|
||||||
|
|
||||||
def get_song_details(self, song_id: str) -> API.Song:
|
def get_song_details(self, song_id: str) -> API.Song:
|
||||||
song = self._get_json(self._make_url("getSong"), id=song_id).song
|
song = self._get_json(self._make_url("getSong"), id=song_id).song
|
||||||
|
@@ -7,6 +7,7 @@ from datetime import timedelta
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import osxmmkeys
|
import osxmmkeys
|
||||||
@@ -35,6 +36,7 @@ except Exception:
|
|||||||
from .adapters import (
|
from .adapters import (
|
||||||
AdapterManager,
|
AdapterManager,
|
||||||
AlbumSearchQuery,
|
AlbumSearchQuery,
|
||||||
|
CacheMissError,
|
||||||
DownloadProgress,
|
DownloadProgress,
|
||||||
Result,
|
Result,
|
||||||
SongCacheStatus,
|
SongCacheStatus,
|
||||||
@@ -469,8 +471,8 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def make_playlist_tuple(p: Playlist) -> GLib.Variant:
|
def make_playlist_tuple(p: Playlist) -> GLib.Variant:
|
||||||
cover_art_filename = AdapterManager.get_cover_art_filename(
|
cover_art_filename = AdapterManager.get_cover_art_uri(
|
||||||
p.cover_art, allow_download=False,
|
p.cover_art, "file", allow_download=False,
|
||||||
).result()
|
).result()
|
||||||
return (f"/playlist/{p.id}", p.name, cover_art_filename or "")
|
return (f"/playlist/{p.id}", p.name, cover_art_filename or "")
|
||||||
|
|
||||||
@@ -1112,9 +1114,27 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
if order_token != self.song_playing_order_token:
|
if order_token != self.song_playing_order_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
# TODO (#189): make this actually use the player's allowed list of schemes
|
uri = None
|
||||||
# to play.
|
try:
|
||||||
uri = AdapterManager.get_song_filename_or_stream(song)
|
if "file" in self.player_manager.supported_schemes:
|
||||||
|
uri = AdapterManager.get_song_file_uri(song)
|
||||||
|
except CacheMissError:
|
||||||
|
logging.debug("Couldn't find the file, will attempt to stream.")
|
||||||
|
|
||||||
|
if not uri:
|
||||||
|
try:
|
||||||
|
uri = AdapterManager.get_song_stream_uri(song)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if (
|
||||||
|
not uri
|
||||||
|
or urlparse(uri).scheme not in self.player_manager.supported_schemes
|
||||||
|
):
|
||||||
|
self.app_config.state.current_notification = UIState.UINotification(
|
||||||
|
markup=f"<b>Unable to play {song.title}.</b>",
|
||||||
|
icon="dialog-error",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Prevent it from doing the thing where it continually loads
|
# Prevent it from doing the thing where it continually loads
|
||||||
# songs when it has to download.
|
# songs when it has to download.
|
||||||
@@ -1165,8 +1185,8 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
)
|
)
|
||||||
song_notification.show()
|
song_notification.show()
|
||||||
|
|
||||||
cover_art_result = AdapterManager.get_cover_art_filename(
|
cover_art_result = AdapterManager.get_cover_art_uri(
|
||||||
song.cover_art
|
song.cover_art, "file"
|
||||||
)
|
)
|
||||||
cover_art_result.add_done_callback(
|
cover_art_result.add_done_callback(
|
||||||
lambda f: on_cover_art_download_complete(f.result())
|
lambda f: on_cover_art_download_complete(f.result())
|
||||||
@@ -1190,7 +1210,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
|
|
||||||
os.system(f"osascript -e '{' '.join(osascript_command)}'")
|
os.system(f"osascript -e '{' '.join(osascript_command)}'")
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(
|
logging.warning(
|
||||||
"Unable to display notification. Is a notification daemon running?" # noqa: E501
|
"Unable to display notification. Is a notification daemon running?" # noqa: E501
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1215,7 +1235,7 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
assert self.player_manager
|
assert self.player_manager
|
||||||
if self.player_manager.can_start_playing_with_no_latency:
|
if self.player_manager.can_start_playing_with_no_latency:
|
||||||
self.player_manager.play_media(
|
self.player_manager.play_media(
|
||||||
AdapterManager.get_song_filename_or_stream(song),
|
AdapterManager.get_song_file_uri(song),
|
||||||
self.app_config.state.song_progress,
|
self.app_config.state.song_progress,
|
||||||
song,
|
song,
|
||||||
)
|
)
|
||||||
|
@@ -111,7 +111,7 @@ def decode_providers(
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
caching_adapter_config=(
|
caching_adapter_config=(
|
||||||
ConfigurationStore(**config.get("caching_adapter_config", {}))
|
ConfigurationStore(**(config.get("caching_adapter_config") or {}))
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for id_, config in providers_dict.items()
|
for id_, config in providers_dict.items()
|
||||||
|
@@ -277,8 +277,8 @@ class DBusManager:
|
|||||||
).result()
|
).result()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cover_art = AdapterManager.get_cover_art_filename(
|
cover_art = AdapterManager.get_cover_art_uri(
|
||||||
playlist.cover_art, allow_download=False
|
playlist.cover_art, "file", allow_download=False
|
||||||
).result()
|
).result()
|
||||||
except CacheMissError:
|
except CacheMissError:
|
||||||
cover_art = ""
|
cover_art = ""
|
||||||
@@ -319,8 +319,8 @@ class DBusManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cover_art = AdapterManager.get_cover_art_filename(
|
cover_art = AdapterManager.get_cover_art_uri(
|
||||||
song.cover_art, allow_download=False
|
song.cover_art, "file", allow_download=False
|
||||||
).result()
|
).result()
|
||||||
except CacheMissError:
|
except CacheMissError:
|
||||||
cover_art = ""
|
cover_art = ""
|
||||||
|
@@ -237,8 +237,7 @@ class ChromecastPlayer(Player):
|
|||||||
song = AdapterManager.get_song_details(
|
song = AdapterManager.get_song_details(
|
||||||
self._serving_song_id.value.decode()
|
self._serving_song_id.value.decode()
|
||||||
).result()
|
).result()
|
||||||
filename = AdapterManager.get_song_filename_or_stream(song)
|
filename = AdapterManager.get_song_file_uri(song)
|
||||||
assert filename.startswith("file://")
|
|
||||||
with open(filename[7:], "rb") as fin:
|
with open(filename[7:], "rb") as fin:
|
||||||
song_buffer = io.BytesIO(fin.read())
|
song_buffer = io.BytesIO(fin.read())
|
||||||
|
|
||||||
@@ -300,7 +299,15 @@ class ChromecastPlayer(Player):
|
|||||||
uri = f"http://{host_ip}:{self.config.get(LAN_PORT_KEY)}/s/{token.decode()}"
|
uri = f"http://{host_ip}:{self.config.get(LAN_PORT_KEY)}/s/{token.decode()}"
|
||||||
logging.info("Serving {song.name} at {uri}")
|
logging.info("Serving {song.name} at {uri}")
|
||||||
|
|
||||||
cover_art_url = AdapterManager.get_cover_art_uri(song.cover_art, size=1000)
|
assert AdapterManager._instance
|
||||||
|
networked_scheme_priority = ("https", "http")
|
||||||
|
scheme = sorted(
|
||||||
|
AdapterManager._instance.ground_truth_adapter.supported_schemes,
|
||||||
|
key=lambda s: networked_scheme_priority.index(s),
|
||||||
|
)[0]
|
||||||
|
cover_art_url = AdapterManager.get_cover_art_uri(
|
||||||
|
song.cover_art, scheme, size=1000
|
||||||
|
)
|
||||||
self._current_chromecast.media_controller.play_media(
|
self._current_chromecast.media_controller.play_media(
|
||||||
uri,
|
uri,
|
||||||
# Just pretend that whatever we send it is mp3, even if it isn't.
|
# Just pretend that whatever we send it is mp3, even if it isn't.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union
|
||||||
|
|
||||||
from sublime.adapters.api_objects import Song
|
from sublime.adapters.api_objects import Song
|
||||||
|
|
||||||
@@ -89,6 +89,12 @@ class PlayerManager:
|
|||||||
if current_player_type := self._get_current_player_type():
|
if current_player_type := self._get_current_player_type():
|
||||||
return self.players.get(current_player_type)
|
return self.players.get(current_player_type)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_schemes(self) -> Set[str]:
|
||||||
|
if cp := self._get_current_player():
|
||||||
|
return cp.supported_schemes
|
||||||
|
return set()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_start_playing_with_no_latency(self) -> bool:
|
def can_start_playing_with_no_latency(self) -> bool:
|
||||||
if self._current_device_id:
|
if self._current_device_id:
|
||||||
|
@@ -839,8 +839,8 @@ class AlbumsGrid(Gtk.Overlay):
|
|||||||
artwork.set_from_file(filename.result())
|
artwork.set_from_file(filename.result())
|
||||||
artwork.set_loading(False)
|
artwork.set_loading(False)
|
||||||
|
|
||||||
cover_art_filename_future = AdapterManager.get_cover_art_filename(
|
cover_art_filename_future = AdapterManager.get_cover_art_uri(
|
||||||
item.album.cover_art
|
item.album.cover_art, "file"
|
||||||
)
|
)
|
||||||
if cover_art_filename_future.data_is_available:
|
if cover_art_filename_future.data_is_available:
|
||||||
on_artwork_downloaded(cover_art_filename_future)
|
on_artwork_downloaded(cover_art_filename_future)
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from functools import partial
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import cast, List, Sequence
|
from typing import cast, List, Sequence
|
||||||
|
|
||||||
@@ -462,7 +463,7 @@ class ArtistDetailPanel(Gtk.Box):
|
|||||||
self.albums_list.update(artist, app_config, force=force)
|
self.albums_list.update(artist, app_config, force=force)
|
||||||
|
|
||||||
@util.async_callback(
|
@util.async_callback(
|
||||||
AdapterManager.get_cover_art_filename,
|
partial(AdapterManager.get_cover_art_uri, scheme="file"),
|
||||||
before_download=lambda self: self.artist_artwork.set_loading(True),
|
before_download=lambda self: self.artist_artwork.set_loading(True),
|
||||||
on_failure=lambda self, e: self.artist_artwork.set_loading(False),
|
on_failure=lambda self, e: self.artist_artwork.set_loading(False),
|
||||||
)
|
)
|
||||||
|
@@ -51,8 +51,10 @@ class AlbumWithSongs(Gtk.Box):
|
|||||||
artist_artwork.set_from_file(f.result())
|
artist_artwork.set_from_file(f.result())
|
||||||
artist_artwork.set_loading(False)
|
artist_artwork.set_loading(False)
|
||||||
|
|
||||||
cover_art_filename_future = AdapterManager.get_cover_art_filename(
|
cover_art_filename_future = AdapterManager.get_cover_art_uri(
|
||||||
album.cover_art, before_download=lambda: artist_artwork.set_loading(True),
|
album.cover_art,
|
||||||
|
"file",
|
||||||
|
before_download=lambda: artist_artwork.set_loading(True),
|
||||||
)
|
)
|
||||||
cover_art_filename_future.add_done_callback(
|
cover_art_filename_future.add_done_callback(
|
||||||
lambda f: GLib.idle_add(cover_art_future_done, f)
|
lambda f: GLib.idle_add(cover_art_future_done, f)
|
||||||
|
@@ -1065,7 +1065,7 @@ class MainWindow(Gtk.ApplicationWindow):
|
|||||||
image.set_loading(False)
|
image.set_loading(False)
|
||||||
image.set_from_file(f.result())
|
image.set_from_file(f.result())
|
||||||
|
|
||||||
artwork_future = AdapterManager.get_cover_art_filename(cover_art_id)
|
artwork_future = AdapterManager.get_cover_art_uri(cover_art_id, "file")
|
||||||
artwork_future.add_done_callback(lambda f: GLib.idle_add(image_callback, f))
|
artwork_future.add_done_callback(lambda f: GLib.idle_add(image_callback, f))
|
||||||
|
|
||||||
return row
|
return row
|
||||||
@@ -1196,7 +1196,7 @@ class DownloadStatusBox(Gtk.Box):
|
|||||||
image.set_loading(False)
|
image.set_loading(False)
|
||||||
image.set_from_file(f.result())
|
image.set_from_file(f.result())
|
||||||
|
|
||||||
artwork_future = AdapterManager.get_cover_art_filename(self.song.cover_art)
|
artwork_future = AdapterManager.get_cover_art_uri(self.song.cover_art, "file")
|
||||||
artwork_future.add_done_callback(lambda f: GLib.idle_add(image_callback, f))
|
artwork_future.add_done_callback(lambda f: GLib.idle_add(image_callback, f))
|
||||||
|
|
||||||
def update_progress(self, progress_fraction: float):
|
def update_progress(self, progress_fraction: float):
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, Optional, Set, Tuple
|
from typing import Any, Callable, Dict, Optional, Set, Tuple
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
def get_cover_art_filename_or_create_future(
|
def get_cover_art_filename_or_create_future(
|
||||||
cover_art_id: Optional[str], idx: int, order_token: int
|
cover_art_id: Optional[str], idx: int, order_token: int
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
cover_art_result = AdapterManager.get_cover_art_filename(cover_art_id)
|
cover_art_result = AdapterManager.get_cover_art_uri(cover_art_id, "file")
|
||||||
if not cover_art_result.data_is_available:
|
if not cover_art_result.data_is_available:
|
||||||
cover_art_result.add_done_callback(
|
cover_art_result.add_done_callback(
|
||||||
make_idle_index_capturing_function(
|
make_idle_index_capturing_function(
|
||||||
@@ -331,7 +331,7 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
self.editing_play_queue_song_list = False
|
self.editing_play_queue_song_list = False
|
||||||
|
|
||||||
@util.async_callback(
|
@util.async_callback(
|
||||||
AdapterManager.get_cover_art_filename,
|
partial(AdapterManager.get_cover_art_uri, scheme="file"),
|
||||||
before_download=lambda self: self.album_art.set_loading(True),
|
before_download=lambda self: self.album_art.set_loading(True),
|
||||||
on_failure=lambda self, e: self.album_art.set_loading(False),
|
on_failure=lambda self, e: self.album_art.set_loading(False),
|
||||||
)
|
)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
from functools import lru_cache
|
from functools import lru_cache, partial
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import Any, cast, Dict, Iterable, List, Tuple
|
from typing import Any, cast, Dict, Iterable, List, Tuple
|
||||||
|
|
||||||
@@ -679,7 +679,7 @@ class PlaylistDetailPanel(Gtk.Overlay):
|
|||||||
self.playlist_action_buttons.show_all()
|
self.playlist_action_buttons.show_all()
|
||||||
|
|
||||||
@util.async_callback(
|
@util.async_callback(
|
||||||
AdapterManager.get_cover_art_filename,
|
partial(AdapterManager.get_cover_art_uri, scheme="file"),
|
||||||
before_download=lambda self: self.playlist_artwork.set_loading(True),
|
before_download=lambda self: self.playlist_artwork.set_loading(True),
|
||||||
on_failure=lambda self, e: self.playlist_artwork.set_loading(False),
|
on_failure=lambda self, e: self.playlist_artwork.set_loading(False),
|
||||||
)
|
)
|
||||||
|
@@ -371,13 +371,13 @@ def test_invalidate_song_file(cache_adapter: FilesystemAdapter):
|
|||||||
cache_adapter.invalidate_data(KEYS.COVER_ART_FILE, "s1")
|
cache_adapter.invalidate_data(KEYS.COVER_ART_FILE, "s1")
|
||||||
|
|
||||||
with pytest.raises(CacheMissError):
|
with pytest.raises(CacheMissError):
|
||||||
cache_adapter.get_song_uri("1", "file")
|
cache_adapter.get_song_file_uri("1", "file")
|
||||||
|
|
||||||
with pytest.raises(CacheMissError):
|
with pytest.raises(CacheMissError):
|
||||||
cache_adapter.get_cover_art_uri("s1", "file", size=300)
|
cache_adapter.get_cover_art_uri("s1", "file", size=300)
|
||||||
|
|
||||||
# Make sure it didn't delete the other song.
|
# Make sure it didn't delete the other song.
|
||||||
assert cache_adapter.get_song_uri("2", "file").endswith("song2.mp3")
|
assert cache_adapter.get_song_file_uri("2", "file").endswith("song2.mp3")
|
||||||
|
|
||||||
|
|
||||||
def test_malformed_song_path(cache_adapter: FilesystemAdapter):
|
def test_malformed_song_path(cache_adapter: FilesystemAdapter):
|
||||||
@@ -390,10 +390,10 @@ def test_malformed_song_path(cache_adapter: FilesystemAdapter):
|
|||||||
KEYS.SONG_FILE, "2", ("fine/path/song2.mp3", MOCK_SONG_FILE2, None)
|
KEYS.SONG_FILE, "2", ("fine/path/song2.mp3", MOCK_SONG_FILE2, None)
|
||||||
)
|
)
|
||||||
|
|
||||||
song_uri = cache_adapter.get_song_uri("1", "file")
|
song_uri = cache_adapter.get_song_file_uri("1", "file")
|
||||||
assert song_uri.endswith(f"/music/{MOCK_SONG_FILE_HASH}")
|
assert song_uri.endswith(f"/music/{MOCK_SONG_FILE_HASH}")
|
||||||
|
|
||||||
song_uri2 = cache_adapter.get_song_uri("2", "file")
|
song_uri2 = cache_adapter.get_song_file_uri("2", "file")
|
||||||
assert song_uri2.endswith("fine/path/song2.mp3")
|
assert song_uri2.endswith("fine/path/song2.mp3")
|
||||||
|
|
||||||
|
|
||||||
@@ -467,7 +467,7 @@ def test_delete_song_data(cache_adapter: FilesystemAdapter):
|
|||||||
KEYS.COVER_ART_FILE, "s1", MOCK_ALBUM_ART,
|
KEYS.COVER_ART_FILE, "s1", MOCK_ALBUM_ART,
|
||||||
)
|
)
|
||||||
|
|
||||||
music_file_path = cache_adapter.get_song_uri("1", "file")
|
music_file_path = cache_adapter.get_song_file_uri("1", "file")
|
||||||
cover_art_path = cache_adapter.get_cover_art_uri("s1", "file", size=300)
|
cover_art_path = cache_adapter.get_cover_art_uri("s1", "file", size=300)
|
||||||
|
|
||||||
cache_adapter.delete_data(KEYS.SONG_FILE, "1")
|
cache_adapter.delete_data(KEYS.SONG_FILE, "1")
|
||||||
@@ -477,7 +477,7 @@ def test_delete_song_data(cache_adapter: FilesystemAdapter):
|
|||||||
assert not Path(cover_art_path).exists()
|
assert not Path(cover_art_path).exists()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cache_adapter.get_song_uri("1", "file")
|
cache_adapter.get_song_file_uri("1", "file")
|
||||||
assert 0, "DID NOT raise CacheMissError"
|
assert 0, "DID NOT raise CacheMissError"
|
||||||
except CacheMissError as e:
|
except CacheMissError as e:
|
||||||
assert e.partial_data is None
|
assert e.partial_data is None
|
||||||
|
Reference in New Issue
Block a user