Improved the way that albums are handled
This commit is contained in:
@@ -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")
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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),
|
||||
|
@@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user