Improved the way that albums are handled

This commit is contained in:
Sumner Evans
2020-05-12 20:28:19 -06:00
parent ce6acc6654
commit 14ffb75063
7 changed files with 126 additions and 96 deletions

View File

@@ -569,18 +569,19 @@ class Adapter(abc.ABC):
"""
raise self._check_can_error("get_ignored_articles")
def get_albums(
self, query: AlbumSearchQuery, limit: int, offset: int
) -> Sequence[Album]:
def get_albums(self, query: AlbumSearchQuery) -> Sequence[Album]:
"""
Get a list of all of the albums known to the adapter for the given query.
.. note::
This request is not paged. You should do any page management to get all of
the albums matching the query internally.
:param query: An :class:`AlbumSearchQuery` object representing the types of
albums to return.
:param limit: The maximum number of albums to return.
:param offset: The index at whith to start returning albums (for paging).
:returns: A list of all of the :class:`sublime.adapter.api_objects.Album`
objects known to the adapter.
objects known to the adapter that match the query.
"""
raise self._check_can_error("get_albums")

View File

@@ -233,12 +233,13 @@ class FilesystemAdapter(CachingAdapter):
models.Artist, artist_id, CachingAdapter.CachedDataKey.ARTIST
)
def get_albums(
self, query: AlbumSearchQuery, limit: int, offset: int,
) -> Sequence[API.Album]:
def get_albums(self, query: AlbumSearchQuery) -> Sequence[API.Album]:
# TODO: deal with ordering
# TODO: deal with paging
# TODO: deal with cache invalidation
sql_query = models.Album.select()
Type = AlbumSearchQuery.Type
Type = AlbumSearchQuery.Type)
if query.type == Type.GENRE:
assert query.genre
genre_name = genre.name if (genre := query.genre) else None
@@ -257,14 +258,12 @@ class FilesystemAdapter(CachingAdapter):
Type.GENRE: sql_query.where(models.Album.genre == genre_name),
}[query.type]
sql_query = sql_query.limit(limit).offset(offset)
if self.is_cache:
# 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.params_hash == util.params_hash(query, limit, offset),
models.CacheInfo.params_hash == util.params_hash(query),
):
raise CacheMissError(partial_data=sql_query)

View File

@@ -430,14 +430,10 @@ class AdapterManager:
logging.info(f"START: {function_name}")
partial_data = None
if AdapterManager._can_use_cache(use_ground_truth_adapter, function_name):
assert AdapterManager._instance.caching_adapter
assert (caching_adapter := AdapterManager._instance.caching_adapter)
try:
logging.info(f"END: TRY SERVE FROM CACHE: {function_name}")
return Result(
getattr(AdapterManager._instance.caching_adapter, function_name)(
*args, **kwargs
)
)
logging.info(f"END: {function_name}: serving from cache")
return Result(getattr(caching_adapter, function_name)(*args, **kwargs))
except CacheMissError as e:
partial_data = e.partial_data
logging.info(f"Cache Miss on {function_name}.")
@@ -952,16 +948,12 @@ class AdapterManager:
@staticmethod
def get_albums(
query: AlbumSearchQuery,
size: int = 40,
offset: int = 0,
before_download: Callable[[], None] = lambda: None,
force: bool = False,
) -> Result[Sequence[Album]]:
return AdapterManager._get_from_cache_or_ground_truth(
"get_albums",
query,
size,
offset,
cache_key=CachingAdapter.CachedDataKey.ALBUMS,
before_download=before_download,
use_ground_truth_adapter=force,

View File

@@ -8,7 +8,18 @@ import random
from datetime import datetime, timedelta
from pathlib import Path
from time import sleep
from typing import Any, cast, Dict, Iterable, Optional, Sequence, Set, Tuple, Union
from typing import (
Any,
cast,
Dict,
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from urllib.parse import urlencode, urlparse
import requests
@@ -360,9 +371,7 @@ class SubsonicAdapter(Adapter):
return set(ignored_articles.split())
def get_albums(
self, query: AlbumSearchQuery, limit: int, offset: int
) -> Sequence[API.Album]:
def get_albums(self, query: AlbumSearchQuery) -> Sequence[API.Album]:
type_ = {
AlbumSearchQuery.Type.RANDOM: "random",
AlbumSearchQuery.Type.NEWEST: "newest",
@@ -377,24 +386,36 @@ class SubsonicAdapter(Adapter):
extra_args: Dict[str, Any] = {}
if query.type == AlbumSearchQuery.Type.YEAR_RANGE:
assert query.year_range
assert (year_range := query.year_range)
extra_args = {
"fromYear": query.year_range[0],
"toYear": query.year_range[1],
"fromYear": year_range[0],
"toYear": year_range[1],
}
elif query.type == AlbumSearchQuery.Type.GENRE:
assert query.genre
extra_args = {"genre": query.genre.name}
assert (genre := query.genre)
extra_args = {"genre": genre.name}
if albums := self._get_json(
self._make_url("getAlbumList2"),
type=type_,
size=limit,
offset=offset,
**extra_args,
).albums:
return albums.album
return []
albums: List[API.Album] = []
page_size = 50 if query.type == AlbumSearchQuery.Type.RANDOM else 500
offset = 0
def get_page(offset: int) -> Sequence[API.Album]:
album_list = self._get_json(
self._make_url("getAlbumList2"),
type=type_,
size=page_size,
offset=offset,
**extra_args,
).albums
return album_list.album if album_list else []
while len(next_page := get_page(offset)) > 0:
albums.extend(next_page)
if query.type == AlbumSearchQuery.Type.RANDOM:
break
offset += page_size
return albums
def get_album(self, album_id: str) -> API.Album:
album = self._get_json(self._make_url("getAlbum"), id=album_id).album

View File

@@ -39,7 +39,7 @@ except Exception:
)
glib_notify_exists = False
from .adapters import AdapterManager, Result
from .adapters import AdapterManager, AlbumSearchQuery, Result
from .adapters.api_objects import Playlist, PlayQueue, Song
from .cache_manager import CacheManager
from .config import AppConfiguration, ReplayGainType
@@ -628,12 +628,13 @@ class SublimeMusicApp(Gtk.Application):
album = AdapterManager.get_album(album_id.get_string()).result()
if year := album.year:
self.app_config.state.current_album_sort = "byYear"
self.app_config.state.current_album_from_year = year
self.app_config.state.current_album_to_year = year
self.app_config.state.current_album_search_query = AlbumSearchQuery(
AlbumSearchQuery.Type.YEAR_RANGE, year_range=(year, year)
)
elif genre := album.genre:
self.app_config.state.current_album_sort = "byGenre"
self.app_config.state.current_album_genre = genre.name
self.app_config.state.current_album_search_query = AlbumSearchQuery(
AlbumSearchQuery.Type.GENRE, genre=genre
)
else:
# TODO (#167) change this to not be a modal dialog.
dialog = Gtk.MessageDialog(
@@ -894,15 +895,17 @@ class SublimeMusicApp(Gtk.Application):
def update_window(self, force: bool = False):
if not self.window:
return
logging.info(f"Updating window force={force}")
GLib.idle_add(lambda: self.window.update(self.app_config, force=force))
def update_play_state_from_server(self, prompt_confirm: bool = False):
# TODO (#129): need to make the play queue list loading for the duration here if
# prompt_confirm is False.
was_playing = self.app_config.state.playing
self.player.pause()
self.app_config.state.playing = False
self.update_window()
if (was_playing := self.app_config.state.playing) :
assert self.player
self.player.pause()
self.app_config.state.playing = False
self.update_window()
def do_update(f: Result[PlayQueue]):
play_queue = f.result()

View File

@@ -71,8 +71,8 @@ class AlbumsPanel(Gtk.Box):
("random", "randomly", True),
("genre", "by genre", AdapterManager.can_get_genres()),
("newest", "by most recently added", True),
# ("highest", "by highest rated", True), # I don't t hink this works
# anyway
# ("highest", "by highest rated", True), # TODO I don't t hink this
# works anyway
("frequent", "by most played", True),
("recent", "by most recently played", True),
("alphabetical", "alphabetically", True),
@@ -176,12 +176,10 @@ class AlbumsPanel(Gtk.Box):
# (En|Dis)able getting genres.
self.sort_type_combo_store[1][2] = AdapterManager.can_get_genres()
self.populate_genre_combo(app_config, force=force)
if app_config:
self.current_query = app_config.state.current_album_search_query
self.sort_type_combo.set_active_id(_to_type(self.current_query.type))
self.alphabetical_type_combo.set_active_id(
{
AlbumSearchQuery.Type.ALPHABETICAL_BY_NAME: "by_name",
@@ -189,11 +187,14 @@ class AlbumsPanel(Gtk.Box):
}.get(self.current_query.type)
or "by_name"
)
self.sort_type_combo.set_active_id(_to_type(self.current_query.type))
if year_range := self.current_query.year_range:
self.from_year_spin_button.set_value(year_range[0])
self.to_year_spin_button.set_value(year_range[1])
self.populate_genre_combo(app_config, force=force)
# Show/hide the combo boxes.
def show_if(sort_type: Iterable[AlbumSearchQuery.Type], *elements):
for element in elements:
@@ -218,6 +219,8 @@ class AlbumsPanel(Gtk.Box):
self.to_year_spin_button,
)
# At this point, the current query should be totally updated.
self.grid_order_token = self.grid.update_params(self.current_query)
self.grid.update(self.grid_order_token, app_config, force=force)
def get_id(self, combo: Gtk.ComboBox) -> Optional[str]:
@@ -238,36 +241,45 @@ class AlbumsPanel(Gtk.Box):
assert id
if id == "alphabetical":
id += "_" + cast(str, self.get_id(self.alphabetical_type_combo))
self.current_query = AlbumSearchQuery(
_from_str(id), self.current_query.year_range, self.current_query.genre
)
self.grid_order_token = self.grid.update_params(self.current_query)
self.emit_if_not_updating(
"refresh-window", {"current_album_search_query": self.current_query}, False,
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
_from_str(id),
self.current_query.year_range,
self.current_query.genre,
)
},
False,
)
def on_alphabetical_type_change(self, combo: Gtk.ComboBox):
id = "alphabetical_" + cast(str, self.get_id(combo))
self.current_query = AlbumSearchQuery(
_from_str(id), self.current_query.year_range, self.current_query.genre
)
self.grid_order_token = self.grid.update_params(self.current_query)
self.emit_if_not_updating(
"refresh-window", {"current_album_search_query": self.current_query}, False,
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
_from_str(id),
self.current_query.year_range,
self.current_query.genre,
)
},
False,
)
def on_genre_change(self, combo: Gtk.ComboBox):
genre = self.get_id(combo)
assert genre
self.current_query = AlbumSearchQuery(
self.current_query.type,
self.current_query.year_range,
AlbumsPanel._Genre(genre),
)
self.grid_order_token = self.grid.update_params(self.current_query)
self.update()
self.emit_if_not_updating(
"refresh-window", {"current_album_search_query": self.current_query}, False,
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
self.current_query.type,
self.current_query.year_range,
AlbumsPanel._Genre(genre),
)
},
False,
)
def on_year_changed(self, entry: Gtk.SpinButton) -> bool:
@@ -278,11 +290,14 @@ class AlbumsPanel(Gtk.Box):
else:
new_year_tuple = (year, self.current_query.year_range[0])
self.current_query = AlbumSearchQuery(
self.current_query.type, new_year_tuple, self.current_query.genre
)
self.emit_if_not_updating(
"refresh-window", {"current_album_search_query": self.current_query}, False,
"refresh-window",
{
"current_album_search_query": AlbumSearchQuery(
self.current_query.type, new_year_tuple, self.current_query.genre
)
},
False,
)
return False
@@ -314,7 +329,8 @@ class AlbumsGrid(Gtk.Overlay):
latest_applied_order_ratchet: int = 0
order_ratchet: int = 0
current_selection: Optional[int] = None
currently_selected_index: Optional[int] = None
currently_selected_id: Optional[str] = None
next_page_fn = None
current_min_size_request = 30
# server_hash = None
@@ -414,13 +430,15 @@ class AlbumsGrid(Gtk.Overlay):
if order_token < self.latest_applied_order_ratchet:
return
if app_config:
self.currently_selected_id = app_config.state.selected_album_id
# TODO test this
# new_hash = app_config.server.strhash()
# server_changed = self.server_hash != new_hash
# self.server_hash = new_hash
self.update_grid(
order_token,
force=force, # or server_changed,
selected_id=app_config.state.selected_album_id,
order_token, force=force, # or server_changed,
)
# Update the detail panel.
@@ -430,9 +448,7 @@ class AlbumsGrid(Gtk.Overlay):
error_dialog = None
def update_grid(
self, order_token: int, force: bool = False, selected_id: str = None
):
def update_grid(self, order_token: int, force: bool = False):
if not AdapterManager.can_get_artists():
self.spinner.hide()
return
@@ -472,18 +488,17 @@ class AlbumsGrid(Gtk.Overlay):
self.list_store.remove_all()
print(selected_id, self.current_selection)
selected_index = None
for i, album in enumerate(albums):
model = AlbumsGrid._AlbumModel(album)
if model.id == selected_id:
if model.id == self.currently_selected_id:
selected_index = i
self.list_store.append(model)
selection_changed = selected_index != self.current_selection
self.current_selection = selected_index
selection_changed = selected_index != self.currently_selected_index
self.currently_selected_index = selected_index
self.reflow_grids(
force_reload_from_master=should_reload,
selection_changed=selection_changed,
@@ -505,7 +520,7 @@ class AlbumsGrid(Gtk.Overlay):
0 if click_top else len(self.list_store_top)
)
if click_top and selected_index == self.current_selection:
if click_top and selected_index == self.currently_selected_index:
self.emit("cover-clicked", None)
else:
self.emit("cover-clicked", self.list_store[selected_index].id)
@@ -586,9 +601,9 @@ class AlbumsGrid(Gtk.Overlay):
):
# Determine where the cuttoff is between the top and bottom grids.
entries_before_fold = len(self.list_store)
if self.current_selection is not None and self.items_per_row:
if self.currently_selected_index is not None and self.items_per_row:
entries_before_fold = (
(self.current_selection // self.items_per_row) + 1
(self.currently_selected_index // self.items_per_row) + 1
) * self.items_per_row
if force_reload_from_master:
@@ -622,8 +637,8 @@ class AlbumsGrid(Gtk.Overlay):
for _ in range(top_diff):
del self.list_store_top[-1]
if self.current_selection is not None:
to_select = self.grid_top.get_child_at_index(self.current_selection)
if self.currently_selected_index is not None:
to_select = self.grid_top.get_child_at_index(self.currently_selected_index)
if not to_select:
return
self.grid_top.select_child(to_select)
@@ -634,7 +649,7 @@ class AlbumsGrid(Gtk.Overlay):
for c in self.detail_box_inner.get_children():
self.detail_box_inner.remove(c)
model = self.list_store[self.current_selection]
model = self.list_store[self.currently_selected_index]
detail_element = AlbumWithSongs(model.album, cover_art_size=300)
detail_element.connect(
"song-clicked", lambda _, *args: self.emit("song-clicked", *args),

View File

@@ -266,7 +266,6 @@ class MainWindow(Gtk.ApplicationWindow):
def search_result_calback(idx: int, result: API.SearchResult):
# Ignore slow returned searches.
print("ohea", idx, self.search_idx)
if idx < self.search_idx:
return