Fixed ordering of albums out of the cache
This commit is contained in:
@@ -127,7 +127,7 @@ class PlayQueue(abc.ABC):
|
|||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=8192)
|
@lru_cache(maxsize=8192)
|
||||||
def similarity_ratio(query: str, string: Optional[str]) -> int:
|
def similarity_ratio(query: str, string: str) -> int:
|
||||||
"""
|
"""
|
||||||
Return the :class:`fuzzywuzzy.fuzz.partial_ratio` between the ``query`` and
|
Return the :class:`fuzzywuzzy.fuzz.partial_ratio` between the ``query`` and
|
||||||
the given ``string``.
|
the given ``string``.
|
||||||
@@ -138,9 +138,7 @@ def similarity_ratio(query: str, string: Optional[str]) -> int:
|
|||||||
:param query: the query string
|
:param query: the query string
|
||||||
:param string: the string to compare to the query string
|
:param string: the string to compare to the query string
|
||||||
"""
|
"""
|
||||||
if not string:
|
return fuzz.partial_ratio(query, string)
|
||||||
return 0
|
|
||||||
return fuzz.partial_ratio(query.lower(), string.lower())
|
|
||||||
|
|
||||||
|
|
||||||
class SearchResult:
|
class SearchResult:
|
||||||
@@ -164,20 +162,29 @@ class SearchResult:
|
|||||||
member = f"_{result_type}"
|
member = f"_{result_type}"
|
||||||
cast(Dict[str, Any], getattr(self, member)).update({r.id: r for r in results})
|
cast(Dict[str, Any], getattr(self, member)).update({r.id: r for r in results})
|
||||||
|
|
||||||
def update(self, search_result: "SearchResult"):
|
def update(self, other: "SearchResult"):
|
||||||
self._artists.update(search_result._artists)
|
assert self.query == other.query
|
||||||
self._albums.update(search_result._albums)
|
self._artists.update(other._artists)
|
||||||
self._songs.update(search_result._songs)
|
self._albums.update(other._albums)
|
||||||
self._playlists.update(search_result._playlists)
|
self._songs.update(other._songs)
|
||||||
|
self._playlists.update(other._playlists)
|
||||||
|
|
||||||
_S = TypeVar("_S")
|
_S = TypeVar("_S")
|
||||||
|
|
||||||
def _to_result(
|
def _to_result(
|
||||||
self, it: Dict[str, _S], transform: Callable[[_S], Tuple[Optional[str], ...]],
|
self, it: Dict[str, _S], transform: Callable[[_S], Tuple[Optional[str], ...]],
|
||||||
) -> List[_S]:
|
) -> List[_S]:
|
||||||
|
assert self.query
|
||||||
all_results = sorted(
|
all_results = sorted(
|
||||||
(
|
(
|
||||||
(max(map(partial(similarity_ratio, self.query), transform(x))), x)
|
(
|
||||||
|
max(
|
||||||
|
partial(similarity_ratio, self.query.lower())(t.lower())
|
||||||
|
for t in transform(x)
|
||||||
|
if t is not None
|
||||||
|
),
|
||||||
|
x,
|
||||||
|
)
|
||||||
for x in it.values()
|
for x in it.values()
|
||||||
),
|
),
|
||||||
key=lambda rx: rx[0],
|
key=lambda rx: rx[0],
|
||||||
|
@@ -4,7 +4,7 @@ import shutil
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, cast, Dict, Optional, Sequence, Set, Tuple, Union
|
from typing import Any, cast, Dict, Optional, Sequence, Set, Union
|
||||||
|
|
||||||
from peewee import fn
|
from peewee import fn
|
||||||
|
|
||||||
@@ -109,12 +109,8 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
model: Any,
|
model: Any,
|
||||||
cache_key: CachingAdapter.CachedDataKey,
|
cache_key: CachingAdapter.CachedDataKey,
|
||||||
ignore_cache_miss: bool = False,
|
ignore_cache_miss: bool = False,
|
||||||
where_clause: Optional[Tuple[Any, ...]] = None,
|
|
||||||
) -> Sequence:
|
) -> Sequence:
|
||||||
query = model.select()
|
result = list(model.select())
|
||||||
if where_clause:
|
|
||||||
query = query.where(*where_clause)
|
|
||||||
result = list(query)
|
|
||||||
if self.is_cache and not ignore_cache_miss:
|
if self.is_cache and not ignore_cache_miss:
|
||||||
# Determine if the adapter has ingested data for this key before, and if
|
# Determine if the adapter has ingested data for this key before, and if
|
||||||
# not, cache miss.
|
# not, cache miss.
|
||||||
@@ -130,10 +126,10 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
model: Any,
|
model: Any,
|
||||||
id: str,
|
id: str,
|
||||||
cache_key: CachingAdapter.CachedDataKey,
|
cache_key: CachingAdapter.CachedDataKey,
|
||||||
where_clause: Tuple[Any, ...] = (),
|
# where_clause: Tuple[Any, ...] = (),
|
||||||
cache_where_clause: Tuple[Any, ...] = (),
|
# cache_where_clause: Tuple[Any, ...] = (),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
obj = model.get_or_none(model.id == id, *where_clause)
|
obj = model.get_or_none(model.id == id)
|
||||||
|
|
||||||
# Handle the case that this is the ground truth adapter.
|
# Handle the case that this is the ground truth adapter.
|
||||||
if not self.is_cache:
|
if not self.is_cache:
|
||||||
@@ -147,7 +143,6 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
models.CacheInfo.cache_key == cache_key,
|
models.CacheInfo.cache_key == cache_key,
|
||||||
models.CacheInfo.parameter == id,
|
models.CacheInfo.parameter == id,
|
||||||
models.CacheInfo.valid == True, # noqa: 712
|
models.CacheInfo.valid == True, # noqa: 712
|
||||||
*cache_where_clause,
|
|
||||||
)
|
)
|
||||||
if not cache_info:
|
if not cache_info:
|
||||||
raise CacheMissError(partial_data=obj)
|
raise CacheMissError(partial_data=obj)
|
||||||
@@ -186,12 +181,19 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
|
|
||||||
return SongCacheStatus.NOT_CACHED
|
return SongCacheStatus.NOT_CACHED
|
||||||
|
|
||||||
|
_playlists = None
|
||||||
|
|
||||||
def get_playlists(self, ignore_cache_miss: bool = False) -> Sequence[API.Playlist]:
|
def get_playlists(self, ignore_cache_miss: bool = False) -> Sequence[API.Playlist]:
|
||||||
return self._get_list(
|
if self._playlists is not None:
|
||||||
|
print('Serving out of RAM')
|
||||||
|
return self._playlists
|
||||||
|
|
||||||
|
self._playlists = self._get_list(
|
||||||
models.Playlist,
|
models.Playlist,
|
||||||
CachingAdapter.CachedDataKey.PLAYLISTS,
|
CachingAdapter.CachedDataKey.PLAYLISTS,
|
||||||
ignore_cache_miss=ignore_cache_miss,
|
ignore_cache_miss=ignore_cache_miss,
|
||||||
)
|
)
|
||||||
|
return self._playlists
|
||||||
|
|
||||||
def get_playlist_details(self, playlist_id: str) -> API.PlaylistDetails:
|
def get_playlist_details(self, playlist_id: str) -> API.PlaylistDetails:
|
||||||
return self._get_object_details(
|
return self._get_object_details(
|
||||||
@@ -237,7 +239,6 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_artists(self, ignore_cache_miss: bool = False) -> Sequence[API.Artist]:
|
def get_artists(self, ignore_cache_miss: bool = False) -> Sequence[API.Artist]:
|
||||||
# TODO order_by
|
|
||||||
return self._get_list(
|
return self._get_list(
|
||||||
models.Artist,
|
models.Artist,
|
||||||
CachingAdapter.CachedDataKey.ARTISTS,
|
CachingAdapter.CachedDataKey.ARTISTS,
|
||||||
@@ -250,11 +251,27 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_albums(self, query: AlbumSearchQuery) -> Sequence[API.Album]:
|
def get_albums(self, query: AlbumSearchQuery) -> Sequence[API.Album]:
|
||||||
# TODO: deal with ordering
|
strhash = query.strhash()
|
||||||
# TODO: deal with paging
|
query_result = models.AlbumQueryResult.get_or_none(
|
||||||
# TODO: deal with cache invalidation
|
models.AlbumQueryResult.query_hash == strhash
|
||||||
|
)
|
||||||
|
# If we've cached the query result, then just return it. If it's stale, then
|
||||||
|
# return the old value as a cache miss error.
|
||||||
|
if query_result and (
|
||||||
|
cache_info := models.CacheInfo.get_or_none(
|
||||||
|
models.CacheInfo.cache_key == CachingAdapter.CachedDataKey.ALBUMS,
|
||||||
|
models.CacheInfo.parameter == strhash,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
if cache_info.valid:
|
||||||
|
return query_result.albums
|
||||||
|
else:
|
||||||
|
raise CacheMissError(partial_data=query_result.albums)
|
||||||
|
|
||||||
|
# If we haven't ever cached the query result, try to construct one, and return
|
||||||
|
# it as a CacheMissError result.
|
||||||
|
|
||||||
sql_query = models.Album.select()
|
sql_query = models.Album.select()
|
||||||
# TODO use the new ``where_clause`` from get_list
|
|
||||||
|
|
||||||
Type = AlbumSearchQuery.Type
|
Type = AlbumSearchQuery.Type
|
||||||
if query.type == Type.GENRE:
|
if query.type == Type.GENRE:
|
||||||
@@ -265,27 +282,20 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
Type.RANDOM: sql_query.order_by(fn.Random()),
|
Type.RANDOM: sql_query.order_by(fn.Random()),
|
||||||
Type.NEWEST: sql_query.order_by(models.Album.created.desc()),
|
Type.NEWEST: sql_query.order_by(models.Album.created.desc()),
|
||||||
Type.FREQUENT: sql_query.order_by(models.Album.play_count.desc()),
|
Type.FREQUENT: sql_query.order_by(models.Album.play_count.desc()),
|
||||||
Type.RECENT: sql_query, # TODO IMPLEMENT
|
Type.STARRED: sql_query.where(models.Album.starred.is_null(False)).order_by(
|
||||||
Type.STARRED: sql_query.where(models.Album.starred.is_null(False)),
|
models.Album.name
|
||||||
|
),
|
||||||
Type.ALPHABETICAL_BY_NAME: sql_query.order_by(models.Album.name),
|
Type.ALPHABETICAL_BY_NAME: sql_query.order_by(models.Album.name),
|
||||||
Type.ALPHABETICAL_BY_ARTIST: sql_query.order_by(models.Album.artist.name),
|
Type.ALPHABETICAL_BY_ARTIST: sql_query.order_by(models.Album.artist.name),
|
||||||
Type.YEAR_RANGE: sql_query.where(
|
Type.YEAR_RANGE: sql_query.where(
|
||||||
models.Album.year.between(*query.year_range)
|
models.Album.year.between(*query.year_range)
|
||||||
).order_by(models.Album.year),
|
).order_by(models.Album.year, models.Album.name),
|
||||||
Type.GENRE: sql_query.where(models.Album.genre == genre_name),
|
Type.GENRE: sql_query.where(models.Album.genre == genre_name).order_by(
|
||||||
}[query.type]
|
models.Album.name
|
||||||
|
),
|
||||||
|
}.get(query.type)
|
||||||
|
|
||||||
if self.is_cache:
|
raise CacheMissError(partial_data=sql_query)
|
||||||
# Determine if the adapter has ingested data for this key before, and if
|
|
||||||
# not, cache miss.
|
|
||||||
if not models.CacheInfo.get_or_none(
|
|
||||||
models.CacheInfo.cache_key == CachingAdapter.CachedDataKey.ALBUMS,
|
|
||||||
models.CacheInfo.parameter == query.strhash(),
|
|
||||||
models.CacheInfo.valid == True, # noqa: 712
|
|
||||||
):
|
|
||||||
raise CacheMissError(partial_data=sql_query)
|
|
||||||
|
|
||||||
return sql_query
|
|
||||||
|
|
||||||
def get_all_albums(self) -> Sequence[API.Album]:
|
def get_all_albums(self) -> Sequence[API.Album]:
|
||||||
return self._get_list(
|
return self._get_list(
|
||||||
@@ -316,7 +326,7 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
return self._get_list(models.Genre, CachingAdapter.CachedDataKey.GENRES)
|
return self._get_list(models.Genre, CachingAdapter.CachedDataKey.GENRES)
|
||||||
|
|
||||||
def search(self, query: str) -> API.SearchResult:
|
def search(self, query: str) -> API.SearchResult:
|
||||||
search_result = API.SearchResult()
|
search_result = API.SearchResult(query)
|
||||||
search_result.add_results("albums", self.get_all_albums())
|
search_result.add_results("albums", self.get_all_albums())
|
||||||
search_result.add_results("artists", self.get_artists(ignore_cache_miss=True))
|
search_result.add_results("artists", self.get_artists(ignore_cache_miss=True))
|
||||||
search_result.add_results(
|
search_result.add_results(
|
||||||
@@ -503,8 +513,6 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
if api_artist.artist_image_url
|
if api_artist.artist_image_url
|
||||||
else None,
|
else None,
|
||||||
}
|
}
|
||||||
# del artist_data["artist_image_url"]
|
|
||||||
# del artist_data["similar_artists"]
|
|
||||||
|
|
||||||
artist, created = models.Artist.get_or_create(
|
artist, created = models.Artist.get_or_create(
|
||||||
id=api_artist.id, defaults=artist_data
|
id=api_artist.id, defaults=artist_data
|
||||||
@@ -605,10 +613,18 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
return_val = ingest_album_data(data)
|
return_val = ingest_album_data(data)
|
||||||
|
|
||||||
elif data_key == KEYS.ALBUMS:
|
elif data_key == KEYS.ALBUMS:
|
||||||
for a in data:
|
albums = [ingest_album_data(a) for a in data]
|
||||||
ingest_album_data(a)
|
album_query_result, created = models.AlbumQueryResult.get_or_create(
|
||||||
# TODO deal with sorting here
|
query_hash=param, defaults={"query_hash": param, "albums": albums}
|
||||||
# TODO need some other way of deleting stale albums
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
album_query_result.albums = albums
|
||||||
|
try:
|
||||||
|
album_query_result.save()
|
||||||
|
except ValueError:
|
||||||
|
# No save necessary.
|
||||||
|
pass
|
||||||
|
|
||||||
elif data_key == KEYS.ARTIST:
|
elif data_key == KEYS.ARTIST:
|
||||||
return_val = ingest_artist_data(data)
|
return_val = ingest_artist_data(data)
|
||||||
@@ -649,6 +665,7 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
return_val = ingest_playlist(data)
|
return_val = ingest_playlist(data)
|
||||||
|
|
||||||
elif data_key == KEYS.PLAYLISTS:
|
elif data_key == KEYS.PLAYLISTS:
|
||||||
|
self._playlists = None
|
||||||
for p in data:
|
for p in data:
|
||||||
ingest_playlist(p)
|
ingest_playlist(p)
|
||||||
models.Playlist.delete().where(
|
models.Playlist.delete().where(
|
||||||
@@ -720,10 +737,6 @@ class FilesystemAdapter(CachingAdapter):
|
|||||||
|
|
||||||
cache_info.save()
|
cache_info.save()
|
||||||
|
|
||||||
# song = models.Song.get_by_id(params[0])
|
|
||||||
# song.file = cache_info
|
|
||||||
# song.save()
|
|
||||||
|
|
||||||
return return_val if return_val is not None else cache_info
|
return return_val if return_val is not None else cache_info
|
||||||
|
|
||||||
def _do_invalidate_data(
|
def _do_invalidate_data(
|
||||||
|
@@ -108,6 +108,11 @@ class Album(BaseModel):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumQueryResult(BaseModel):
|
||||||
|
query_hash = TextField(primary_key=True)
|
||||||
|
albums = SortedManyToManyField(Album)
|
||||||
|
|
||||||
|
|
||||||
class IgnoredArticle(BaseModel):
|
class IgnoredArticle(BaseModel):
|
||||||
name = TextField(unique=True, primary_key=True)
|
name = TextField(unique=True, primary_key=True)
|
||||||
|
|
||||||
@@ -213,6 +218,8 @@ class Version(BaseModel):
|
|||||||
|
|
||||||
ALL_TABLES = (
|
ALL_TABLES = (
|
||||||
Album,
|
Album,
|
||||||
|
AlbumQueryResult,
|
||||||
|
AlbumQueryResult.albums.get_through_model(),
|
||||||
Artist,
|
Artist,
|
||||||
CacheInfo,
|
CacheInfo,
|
||||||
Directory,
|
Directory,
|
||||||
|
@@ -148,7 +148,7 @@ class Result(Generic[T]):
|
|||||||
|
|
||||||
class AdapterManager:
|
class AdapterManager:
|
||||||
available_adapters: Set[Any] = {FilesystemAdapter, SubsonicAdapter}
|
available_adapters: Set[Any] = {FilesystemAdapter, SubsonicAdapter}
|
||||||
current_download_uris: Set[str] = set()
|
current_download_ids: Set[str] = set()
|
||||||
download_set_lock = threading.Lock()
|
download_set_lock = threading.Lock()
|
||||||
executor: ThreadPoolExecutor = ThreadPoolExecutor()
|
executor: ThreadPoolExecutor = ThreadPoolExecutor()
|
||||||
download_executor: ThreadPoolExecutor = ThreadPoolExecutor()
|
download_executor: ThreadPoolExecutor = ThreadPoolExecutor()
|
||||||
@@ -304,7 +304,7 @@ class AdapterManager:
|
|||||||
return Result(future_fn)
|
return Result(future_fn)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_download_fn(uri: str) -> Callable[[], str]:
|
def _create_download_fn(uri: str, id: str) -> Callable[[], str]:
|
||||||
"""
|
"""
|
||||||
Create a function to download the given URI to a temporary file, and return the
|
Create a function to download the given URI to a temporary file, and return the
|
||||||
filename. The returned function will spin-loop if the resource is already being
|
filename. The returned function will spin-loop if the resource is already being
|
||||||
@@ -319,9 +319,9 @@ class AdapterManager:
|
|||||||
|
|
||||||
resource_downloading = False
|
resource_downloading = False
|
||||||
with AdapterManager.download_set_lock:
|
with AdapterManager.download_set_lock:
|
||||||
if uri in AdapterManager.current_download_uris:
|
if id in AdapterManager.current_download_ids:
|
||||||
resource_downloading = True
|
resource_downloading = True
|
||||||
AdapterManager.current_download_uris.add(uri)
|
AdapterManager.current_download_ids.add(id)
|
||||||
|
|
||||||
# TODO (#122): figure out how to retry if the other request failed.
|
# TODO (#122): figure out how to retry if the other request failed.
|
||||||
if resource_downloading:
|
if resource_downloading:
|
||||||
@@ -331,7 +331,7 @@ class AdapterManager:
|
|||||||
# it has completed. Then, just return the path to the
|
# it has completed. Then, just return the path to the
|
||||||
# resource.
|
# resource.
|
||||||
t = 0.0
|
t = 0.0
|
||||||
while uri in AdapterManager.current_download_uris and t < 20:
|
while id in AdapterManager.current_download_ids and t < 20:
|
||||||
sleep(0.2)
|
sleep(0.2)
|
||||||
t += 0.2
|
t += 0.2
|
||||||
# TODO (#122): handle the timeout
|
# TODO (#122): handle the timeout
|
||||||
@@ -351,7 +351,7 @@ class AdapterManager:
|
|||||||
finally:
|
finally:
|
||||||
# Always release the download set lock, even if there's an error.
|
# Always release the download set lock, even if there's an error.
|
||||||
with AdapterManager.download_set_lock:
|
with AdapterManager.download_set_lock:
|
||||||
AdapterManager.current_download_uris.discard(uri)
|
AdapterManager.current_download_ids.discard(id)
|
||||||
|
|
||||||
logging.info(f"{uri} downloaded. Returning.")
|
logging.info(f"{uri} downloaded. Returning.")
|
||||||
return str(download_tmp_filename)
|
return str(download_tmp_filename)
|
||||||
@@ -410,7 +410,7 @@ class AdapterManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_from_cache_or_ground_truth(
|
def _get_from_cache_or_ground_truth(
|
||||||
function_name: str,
|
function_name: str,
|
||||||
param: Optional[str],
|
param: Optional[Union[str, AlbumSearchQuery]],
|
||||||
cache_key: CachingAdapter.CachedDataKey = None,
|
cache_key: CachingAdapter.CachedDataKey = None,
|
||||||
before_download: Callable[[], None] = None,
|
before_download: Callable[[], None] = None,
|
||||||
use_ground_truth_adapter: bool = False,
|
use_ground_truth_adapter: bool = False,
|
||||||
@@ -440,6 +440,8 @@ class AdapterManager:
|
|||||||
assert (caching_adapter := AdapterManager._instance.caching_adapter)
|
assert (caching_adapter := AdapterManager._instance.caching_adapter)
|
||||||
try:
|
try:
|
||||||
logging.info(f"END: {function_name}: serving from cache")
|
logging.info(f"END: {function_name}: serving from cache")
|
||||||
|
if param is None:
|
||||||
|
return Result(getattr(caching_adapter, function_name)(**kwargs))
|
||||||
return Result(getattr(caching_adapter, function_name)(param, **kwargs))
|
return Result(getattr(caching_adapter, function_name)(param, **kwargs))
|
||||||
except CacheMissError as e:
|
except CacheMissError as e:
|
||||||
partial_data = e.partial_data
|
partial_data = e.partial_data
|
||||||
@@ -447,12 +449,18 @@ class AdapterManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(f"Error on {function_name} retrieving from cache.")
|
logging.exception(f"Error on {function_name} retrieving from cache.")
|
||||||
|
|
||||||
|
param_str = param.strhash() if isinstance(param, AlbumSearchQuery) else param
|
||||||
if (
|
if (
|
||||||
cache_key
|
cache_key
|
||||||
and AdapterManager._instance.caching_adapter
|
and AdapterManager._instance.caching_adapter
|
||||||
and use_ground_truth_adapter
|
and use_ground_truth_adapter
|
||||||
):
|
):
|
||||||
AdapterManager._instance.caching_adapter.invalidate_data(cache_key, param)
|
AdapterManager._instance.caching_adapter.invalidate_data(
|
||||||
|
cache_key, param_str
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO If any of the following fails, do we want to return what the caching
|
||||||
|
# adapter has?
|
||||||
|
|
||||||
# TODO (#188): don't short circuit if not allow_download because it could be the
|
# TODO (#188): don't short circuit if not allow_download because it could be the
|
||||||
# filesystem adapter.
|
# filesystem adapter.
|
||||||
@@ -473,7 +481,7 @@ class AdapterManager:
|
|||||||
if AdapterManager._instance.caching_adapter:
|
if AdapterManager._instance.caching_adapter:
|
||||||
if cache_key:
|
if cache_key:
|
||||||
result.add_done_callback(
|
result.add_done_callback(
|
||||||
AdapterManager._create_caching_done_callback(cache_key, param)
|
AdapterManager._create_caching_done_callback(cache_key, param_str)
|
||||||
)
|
)
|
||||||
|
|
||||||
if on_result_finished:
|
if on_result_finished:
|
||||||
@@ -708,6 +716,7 @@ class AdapterManager:
|
|||||||
AdapterManager._instance.ground_truth_adapter.get_cover_art_uri(
|
AdapterManager._instance.ground_truth_adapter.get_cover_art_uri(
|
||||||
cover_art_id, AdapterManager._get_scheme()
|
cover_art_id, AdapterManager._get_scheme()
|
||||||
),
|
),
|
||||||
|
cover_art_id,
|
||||||
),
|
),
|
||||||
is_download=True,
|
is_download=True,
|
||||||
default_value=existing_cover_art_filename,
|
default_value=existing_cover_art_filename,
|
||||||
@@ -802,6 +811,7 @@ class AdapterManager:
|
|||||||
AdapterManager._instance.ground_truth_adapter.get_song_uri(
|
AdapterManager._instance.ground_truth_adapter.get_song_uri(
|
||||||
song_id, AdapterManager._get_scheme()
|
song_id, AdapterManager._get_scheme()
|
||||||
),
|
),
|
||||||
|
song_id,
|
||||||
)()
|
)()
|
||||||
AdapterManager._instance.caching_adapter.ingest_new_data(
|
AdapterManager._instance.caching_adapter.ingest_new_data(
|
||||||
CachingAdapter.CachedDataKey.SONG_FILE,
|
CachingAdapter.CachedDataKey.SONG_FILE,
|
||||||
@@ -998,7 +1008,7 @@ class AdapterManager:
|
|||||||
) -> Result[Sequence[Album]]:
|
) -> Result[Sequence[Album]]:
|
||||||
return AdapterManager._get_from_cache_or_ground_truth(
|
return AdapterManager._get_from_cache_or_ground_truth(
|
||||||
"get_albums",
|
"get_albums",
|
||||||
query.strhash(),
|
query,
|
||||||
cache_key=CachingAdapter.CachedDataKey.ALBUMS,
|
cache_key=CachingAdapter.CachedDataKey.ALBUMS,
|
||||||
before_download=before_download,
|
before_download=before_download,
|
||||||
use_ground_truth_adapter=force,
|
use_ground_truth_adapter=force,
|
||||||
@@ -1162,7 +1172,7 @@ class AdapterManager:
|
|||||||
if not AdapterManager._instance.caching_adapter:
|
if not AdapterManager._instance.caching_adapter:
|
||||||
return SongCacheStatus.NOT_CACHED
|
return SongCacheStatus.NOT_CACHED
|
||||||
|
|
||||||
if song.id in AdapterManager.current_download_uris:
|
if song.id in AdapterManager.current_download_ids:
|
||||||
return SongCacheStatus.DOWNLOADING
|
return SongCacheStatus.DOWNLOADING
|
||||||
|
|
||||||
return AdapterManager._instance.caching_adapter.get_cached_status(song)
|
return AdapterManager._instance.caching_adapter.get_cached_status(song)
|
||||||
|
@@ -568,25 +568,21 @@ class SublimeMusicApp(Gtk.Application):
|
|||||||
def on_shuffle_press(self, *args):
|
def on_shuffle_press(self, *args):
|
||||||
if self.app_config.state.shuffle_on:
|
if self.app_config.state.shuffle_on:
|
||||||
# Revert to the old play queue.
|
# Revert to the old play queue.
|
||||||
old_play_queue_copy = self.app_config.state.old_play_queue.copy()
|
old_play_queue_copy = self.app_config.state.old_play_queue
|
||||||
self.app_config.state.current_song_index = old_play_queue_copy.index(
|
self.app_config.state.current_song_index = old_play_queue_copy.index(
|
||||||
self.app_config.state.current_song.id
|
self.app_config.state.current_song.id
|
||||||
)
|
)
|
||||||
self.app_config.state.play_queue = old_play_queue_copy
|
self.app_config.state.play_queue = old_play_queue_copy
|
||||||
else:
|
else:
|
||||||
self.app_config.state.old_play_queue = (
|
self.app_config.state.old_play_queue = self.app_config.state.play_queue
|
||||||
self.app_config.state.play_queue.copy()
|
|
||||||
)
|
mutable_play_queue = list(self.app_config.state.play_queue)
|
||||||
|
|
||||||
# Remove the current song, then shuffle and put the song back.
|
# Remove the current song, then shuffle and put the song back.
|
||||||
song_id = self.app_config.state.current_song.id
|
song_id = self.app_config.state.current_song.id
|
||||||
del self.app_config.state.play_queue[
|
del mutable_play_queue[self.app_config.state.current_song_index]
|
||||||
self.app_config.state.current_song_index
|
random.shuffle(mutable_play_queue)
|
||||||
]
|
self.app_config.state.play_queue = (song_id,) + tuple(mutable_play_queue)
|
||||||
random.shuffle(self.app_config.state.play_queue)
|
|
||||||
self.app_config.state.play_queue = [
|
|
||||||
song_id
|
|
||||||
] + self.app_config.state.play_queue
|
|
||||||
self.app_config.state.current_song_index = 0
|
self.app_config.state.current_song_index = 0
|
||||||
|
|
||||||
self.app_config.state.shuffle_on = not self.app_config.state.shuffle_on
|
self.app_config.state.shuffle_on = not self.app_config.state.shuffle_on
|
||||||
|
@@ -182,7 +182,7 @@ class DBusManager:
|
|||||||
if get_playlists_result.data_is_available:
|
if get_playlists_result.data_is_available:
|
||||||
playlist_count = len(get_playlists_result.result())
|
playlist_count = len(get_playlists_result.result())
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Couldn't get playlists")
|
pass
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"org.mpris.MediaPlayer2": {
|
"org.mpris.MediaPlayer2": {
|
||||||
|
@@ -469,7 +469,7 @@ class AlbumsGrid(Gtk.Overlay):
|
|||||||
)
|
)
|
||||||
self.error_dialog.format_secondary_markup(
|
self.error_dialog.format_secondary_markup(
|
||||||
# TODO make this error better
|
# TODO make this error better
|
||||||
f"Getting albums by {self.current_query.type} failed due to the"
|
f"Getting albums by {self.current_query.type} failed due to the "
|
||||||
f"following error\n\n{e}"
|
f"following error\n\n{e}"
|
||||||
)
|
)
|
||||||
self.error_dialog.run()
|
self.error_dialog.run()
|
||||||
|
@@ -102,8 +102,6 @@ class ListAndDrilldown(Gtk.Paned):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
id_stack = None
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
Gtk.Paned.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
|
|
||||||
@@ -116,8 +114,8 @@ class ListAndDrilldown(Gtk.Paned):
|
|||||||
)
|
)
|
||||||
self.pack1(self.list, False, False)
|
self.pack1(self.list, False, False)
|
||||||
|
|
||||||
self.drilldown = Gtk.Box()
|
self.box = Gtk.Box()
|
||||||
self.pack2(self.drilldown, True, False)
|
self.pack2(self.box, True, False)
|
||||||
|
|
||||||
def update(
|
def update(
|
||||||
self,
|
self,
|
||||||
@@ -125,8 +123,7 @@ class ListAndDrilldown(Gtk.Paned):
|
|||||||
app_config: AppConfiguration,
|
app_config: AppConfiguration,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
*rest, dir_id = id_stack
|
*child_id_stack, dir_id = id_stack
|
||||||
child_id_stack = tuple(rest)
|
|
||||||
selected_id = child_id_stack[-1] if len(child_id_stack) > 0 else None
|
selected_id = child_id_stack[-1] if len(child_id_stack) > 0 else None
|
||||||
|
|
||||||
self.list.update(
|
self.list.update(
|
||||||
@@ -136,26 +133,26 @@ class ListAndDrilldown(Gtk.Paned):
|
|||||||
force=force,
|
force=force,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.id_stack == id_stack:
|
children = self.box.get_children()
|
||||||
# We always want to update, but in this case, we don't want to blow
|
if len(child_id_stack) == 0:
|
||||||
# away the drilldown.
|
if len(children) > 0:
|
||||||
if isinstance(self.drilldown, ListAndDrilldown):
|
self.box.remove(children[0])
|
||||||
self.drilldown.update(child_id_stack, app_config, force=force)
|
else:
|
||||||
return
|
if len(children) == 0:
|
||||||
self.id_stack = id_stack
|
drilldown = ListAndDrilldown()
|
||||||
|
drilldown.connect(
|
||||||
|
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
||||||
|
)
|
||||||
|
drilldown.connect(
|
||||||
|
"refresh-window",
|
||||||
|
lambda _, *args: self.emit("refresh-window", *args),
|
||||||
|
)
|
||||||
|
self.box.add(drilldown)
|
||||||
|
self.box.show_all()
|
||||||
|
|
||||||
if len(child_id_stack) > 0:
|
self.box.get_children()[0].update(
|
||||||
self.remove(self.drilldown)
|
tuple(child_id_stack), app_config, force=force
|
||||||
self.drilldown = ListAndDrilldown()
|
|
||||||
self.drilldown.connect(
|
|
||||||
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),
|
|
||||||
)
|
)
|
||||||
self.drilldown.connect(
|
|
||||||
"refresh-window", lambda _, *args: self.emit("refresh-window", *args),
|
|
||||||
)
|
|
||||||
self.drilldown.update(child_id_stack, app_config, force=force)
|
|
||||||
self.drilldown.show_all()
|
|
||||||
self.pack2(self.drilldown, True, False)
|
|
||||||
|
|
||||||
|
|
||||||
class MusicDirectoryList(Gtk.Box):
|
class MusicDirectoryList(Gtk.Box):
|
||||||
|
@@ -181,6 +181,8 @@ class PlayerControls(Gtk.ActionBar):
|
|||||||
f"<b>Play Queue:</b> {play_queue_len} {song_label}"
|
f"<b>Play Queue:</b> {play_queue_len} {song_label}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO this is super freaking stupid inefficient.
|
||||||
|
# IDEAS: batch it, don't get the queue until requested
|
||||||
self.editing_play_queue_song_list = True
|
self.editing_play_queue_song_list = True
|
||||||
|
|
||||||
new_store = []
|
new_store = []
|
||||||
|
@@ -226,6 +226,7 @@ def show_song_popover(
|
|||||||
download_sensitive, remove_download_sensitive = False, False
|
download_sensitive, remove_download_sensitive = False, False
|
||||||
albums, artists, parents = set(), set(), set()
|
albums, artists, parents = set(), set(), set()
|
||||||
for song_id in song_ids:
|
for song_id in song_ids:
|
||||||
|
# TODO lazy load these
|
||||||
details = AdapterManager.get_song_details(song_id).result()
|
details = AdapterManager.get_song_details(song_id).result()
|
||||||
status = AdapterManager.get_cached_status(details)
|
status = AdapterManager.get_cached_status(details)
|
||||||
albums.add(album.id if (album := details.album) else None)
|
albums.add(album.id if (album := details.album) else None)
|
||||||
|
@@ -885,6 +885,39 @@ def test_get_music_directory(cache_adapter: FilesystemAdapter):
|
|||||||
assert dir_child.name == "Crash My Party"
|
assert dir_child.name == "Crash My Party"
|
||||||
|
|
||||||
|
|
||||||
def test_search(adapter: FilesystemAdapter):
|
def test_search(cache_adapter: FilesystemAdapter):
|
||||||
# TODO
|
with pytest.raises(CacheMissError):
|
||||||
pass
|
cache_adapter.get_artist("artist1")
|
||||||
|
with pytest.raises(CacheMissError):
|
||||||
|
cache_adapter.get_album("album1")
|
||||||
|
with pytest.raises(CacheMissError):
|
||||||
|
cache_adapter.get_song_details("s1")
|
||||||
|
|
||||||
|
search_result = SublimeAPI.SearchResult()
|
||||||
|
search_result.add_results(
|
||||||
|
"albums",
|
||||||
|
[
|
||||||
|
SubsonicAPI.Album("album1", "Foo", artist_id="artist1", cover_art="cal1"),
|
||||||
|
SubsonicAPI.Album("album2", "Boo", artist_id="artist1", cover_art="cal2"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
search_result.add_results(
|
||||||
|
"artists",
|
||||||
|
[
|
||||||
|
SubsonicAPI.ArtistAndArtistInfo("artist1", "foo", cover_art="car1"),
|
||||||
|
SubsonicAPI.ArtistAndArtistInfo("artist2", "better boo", cover_art="car2"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
search_result.add_results(
|
||||||
|
"songs",
|
||||||
|
[
|
||||||
|
SubsonicAPI.Song("s1", "amazing boo", cover_art="s1"),
|
||||||
|
SubsonicAPI.Song("s2", "foo of all foo", cover_art="s2"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
cache_adapter.ingest_new_data(KEYS.SEARCH_RESULTS, None, search_result)
|
||||||
|
|
||||||
|
search_result = cache_adapter.search("foo")
|
||||||
|
assert [s.title for s in search_result.songs] == ["foo of all foo", "amazing boo"]
|
||||||
|
assert [a.name for a in search_result.artists] == ["foo", "better boo"]
|
||||||
|
assert [a.name for a in search_result.albums] == ["Foo", "Boo"]
|
||||||
|
Reference in New Issue
Block a user