Tons of improvements to browsing using the Albums view

This commit is contained in:
Sumner Evans
2019-08-29 23:03:30 -06:00
parent db7eec51a3
commit 0d5fed0f4e
6 changed files with 302 additions and 20 deletions

View File

@@ -100,6 +100,9 @@ def get_dependencies(xs_el) -> Tuple[Set[str], Dict[str, str]]:
# <complexType>s depend on all of the types that their children have.
for el in xs_el.getchildren():
deps, fields = get_dependencies(el)
# Genres has this.
fields['value'] = 'str'
depends_on |= deps
type_fields.update(fields)

View File

@@ -31,6 +31,7 @@ from libremsonic.server.api_objects import (
Playlist,
PlaylistWithSongs,
Child,
Genre,
# Non-ID3 versions
Artist,
@@ -146,6 +147,7 @@ class CacheManager(metaclass=Singleton):
('playlists', Playlist, list),
('playlist_details', PlaylistWithSongs, dict),
('song_details', Child, dict),
('genres', Genre, list),
# Non-ID3 caches
('albums', Child, list),
@@ -425,17 +427,22 @@ class CacheManager(metaclass=Singleton):
def get_albums(
self,
type_: str,
size: int = 30,
before_download: Callable[[], None] = lambda: None,
force: bool = False,
# Look at documentation for get_album_list in server.py:
**params,
) -> Future:
def do_get_albums() -> List[Child]:
cache_name = self.id3ify('albums')
server_fn = (self.server.get_album_list2 if self.browse_by_tags
else self.server.get_album_list)
# TODO cache per type.
# TODO handle random.
if not self.cache.get(cache_name) or force:
before_download()
albums = server_fn(type_)
albums = server_fn(type_, size=size, **params)
with self.cache_lock:
self.cache[cache_name] = albums.album
@@ -578,6 +585,22 @@ class CacheManager(metaclass=Singleton):
return (str(abs_path), False)
def get_genres(self,
before_download: Callable[[], None] = lambda: None,
force: bool = False,
) -> Future:
def do_get_genres() -> List[Genre]:
if not self.cache['genres'] or force:
before_download()
genres = self.server.get_genres().genre
with self.cache_lock:
self.cache['genres'] = genres
self.save_cache_info()
return self.cache['genres']
return CacheManager.executor.submit(do_get_genres)
def get_cached_status(self, song: Child) -> SongCacheStatus:
cache_path = self.calculate_abs_path(song.path)
if cache_path.exists():

View File

@@ -18,6 +18,7 @@ class AlbumInfo(APIObject):
smallImageUrl: List[str]
mediumImageUrl: List[str]
largeImageUrl: List[str]
value: str
class AverageRating(APIObject, float):
@@ -37,6 +38,7 @@ class UserRating(APIObject, int):
class Child(APIObject):
id: str
value: str
parent: str
isDir: bool
title: str
@@ -71,10 +73,12 @@ class Child(APIObject):
class AlbumList(APIObject):
album: List[Child]
value: str
class AlbumID3(APIObject):
id: str
value: str
name: str
artist: str
artistId: str
@@ -90,10 +94,12 @@ class AlbumID3(APIObject):
class AlbumList2(APIObject):
album: List[AlbumID3]
value: str
class AlbumWithSongsID3(APIObject):
song: List[Child]
value: str
id: str
name: str
artist: str
@@ -110,6 +116,7 @@ class AlbumWithSongsID3(APIObject):
class Artist(APIObject):
id: str
value: str
name: str
artistImageUrl: str
starred: datetime
@@ -124,10 +131,12 @@ class ArtistInfoBase(APIObject):
smallImageUrl: List[str]
mediumImageUrl: List[str]
largeImageUrl: List[str]
value: str
class ArtistInfo(APIObject):
similarArtist: List[Artist]
value: str
biography: List[str]
musicBrainzId: List[str]
lastFmUrl: List[str]
@@ -138,6 +147,7 @@ class ArtistInfo(APIObject):
class ArtistID3(APIObject):
id: str
value: str
name: str
coverArt: str
artistImageUrl: str
@@ -147,6 +157,7 @@ class ArtistID3(APIObject):
class ArtistInfo2(APIObject):
similarArtist: List[ArtistID3]
value: str
biography: List[str]
musicBrainzId: List[str]
lastFmUrl: List[str]
@@ -157,6 +168,7 @@ class ArtistInfo2(APIObject):
class ArtistWithAlbumsID3(APIObject):
album: List[AlbumID3]
value: str
id: str
name: str
coverArt: str
@@ -167,16 +179,19 @@ class ArtistWithAlbumsID3(APIObject):
class IndexID3(APIObject):
artist: List[ArtistID3]
value: str
name: str
class ArtistsID3(APIObject):
index: List[IndexID3]
value: str
ignoredArticles: str
class Bookmark(APIObject):
entry: List[Child]
value: str
position: int
username: str
comment: str
@@ -186,20 +201,24 @@ class Bookmark(APIObject):
class Bookmarks(APIObject):
bookmark: List[Bookmark]
value: str
class ChatMessage(APIObject):
username: str
value: str
time: int
message: str
class ChatMessages(APIObject):
chatMessage: List[ChatMessage]
value: str
class Directory(APIObject):
child: List[Child]
value: str
id: str
parent: str
name: str
@@ -211,20 +230,24 @@ class Directory(APIObject):
class Error(APIObject):
code: int
value: str
message: str
class Genre(APIObject):
songCount: int
value: str
albumCount: int
class Genres(APIObject):
genre: List[Genre]
value: str
class Index(APIObject):
artist: List[Artist]
value: str
name: str
@@ -232,12 +255,14 @@ class Indexes(APIObject):
shortcut: List[Artist]
index: List[Index]
child: List[Child]
value: str
lastModified: int
ignoredArticles: str
class InternetRadioStation(APIObject):
id: str
value: str
name: str
streamUrl: str
homePageUrl: str
@@ -245,10 +270,12 @@ class InternetRadioStation(APIObject):
class InternetRadioStations(APIObject):
internetRadioStation: List[InternetRadioStation]
value: str
class JukeboxStatus(APIObject):
currentIndex: int
value: str
playing: bool
gain: float
position: int
@@ -256,6 +283,7 @@ class JukeboxStatus(APIObject):
class JukeboxPlaylist(APIObject):
entry: List[Child]
value: str
currentIndex: int
playing: bool
gain: float
@@ -264,6 +292,7 @@ class JukeboxPlaylist(APIObject):
class License(APIObject):
valid: bool
value: str
email: str
licenseExpires: datetime
trialExpires: datetime
@@ -271,16 +300,19 @@ class License(APIObject):
class Lyrics(APIObject):
artist: str
value: str
title: str
class MusicFolder(APIObject):
id: int
value: str
name: str
class MusicFolders(APIObject):
musicFolder: List[MusicFolder]
value: str
class PodcastStatus(APIObject, Enum):
@@ -298,6 +330,7 @@ class PodcastEpisode(APIObject):
description: str
status: PodcastStatus
publishDate: datetime
value: str
id: str
parent: str
isDir: bool
@@ -333,6 +366,7 @@ class PodcastEpisode(APIObject):
class NewestPodcasts(APIObject):
episode: List[PodcastEpisode]
value: str
class NowPlayingEntry(APIObject):
@@ -340,6 +374,7 @@ class NowPlayingEntry(APIObject):
minutesAgo: int
playerId: int
playerName: str
value: str
id: str
parent: str
isDir: bool
@@ -375,10 +410,12 @@ class NowPlayingEntry(APIObject):
class NowPlaying(APIObject):
entry: List[NowPlayingEntry]
value: str
class PlayQueue(APIObject):
entry: List[Child]
value: str
current: int
position: int
username: str
@@ -388,6 +425,7 @@ class PlayQueue(APIObject):
class Playlist(APIObject):
allowedUser: List[str]
value: str
id: str
name: str
comment: str
@@ -402,6 +440,7 @@ class Playlist(APIObject):
class PlaylistWithSongs(APIObject):
entry: List[Child]
value: str
allowedUser: List[str]
id: str
name: str
@@ -417,10 +456,12 @@ class PlaylistWithSongs(APIObject):
class Playlists(APIObject):
playlist: List[Playlist]
value: str
class PodcastChannel(APIObject):
episode: List[PodcastEpisode]
value: str
id: str
url: str
title: str
@@ -433,6 +474,7 @@ class PodcastChannel(APIObject):
class Podcasts(APIObject):
channel: List[PodcastChannel]
value: str
class ResponseStatus(APIObject, Enum):
@@ -442,11 +484,13 @@ class ResponseStatus(APIObject, Enum):
class ScanStatus(APIObject):
scanning: bool
value: str
count: int
class SearchResult(APIObject):
match: List[Child]
value: str
offset: int
totalHits: int
@@ -455,16 +499,19 @@ class SearchResult2(APIObject):
artist: List[Artist]
album: List[Child]
song: List[Child]
value: str
class SearchResult3(APIObject):
artist: List[ArtistID3]
album: List[AlbumID3]
song: List[Child]
value: str
class Share(APIObject):
entry: List[Child]
value: str
id: str
url: str
description: str
@@ -477,38 +524,46 @@ class Share(APIObject):
class Shares(APIObject):
share: List[Share]
value: str
class SimilarSongs(APIObject):
song: List[Child]
value: str
class SimilarSongs2(APIObject):
song: List[Child]
value: str
class Songs(APIObject):
song: List[Child]
value: str
class Starred(APIObject):
artist: List[Artist]
album: List[Child]
song: List[Child]
value: str
class Starred2(APIObject):
artist: List[ArtistID3]
album: List[AlbumID3]
song: List[Child]
value: str
class TopSongs(APIObject):
song: List[Child]
value: str
class User(APIObject):
folder: List[int]
value: str
username: str
email: str
scrobblingEnabled: bool
@@ -530,6 +585,7 @@ class User(APIObject):
class Users(APIObject):
user: List[User]
value: str
class Version(APIObject, str):
@@ -538,17 +594,20 @@ class Version(APIObject, str):
class AudioTrack(APIObject):
id: str
value: str
name: str
languageCode: str
class Captions(APIObject):
id: str
value: str
name: str
class VideoConversion(APIObject):
id: str
value: str
bitRate: int
audioTrackId: int
@@ -557,11 +616,13 @@ class VideoInfo(APIObject):
captions: List[Captions]
audioTrack: List[AudioTrack]
conversion: List[VideoConversion]
value: str
id: str
class Videos(APIObject):
video: List[Child]
value: str
class Response(APIObject):
@@ -608,5 +669,6 @@ class Response(APIObject):
topSongs: TopSongs
scanStatus: ScanStatus
error: Error
value: str
status: ResponseStatus
version: Version

View File

@@ -2,7 +2,7 @@ import gi
from typing import Optional, Union
gi.require_version('Gtk', '3.0')
from gi.repository import Gio, Gtk, GObject
from gi.repository import Gtk, GObject, GLib
from libremsonic.state_manager import ApplicationState
from libremsonic.cache_manager import CacheManager
@@ -14,7 +14,7 @@ from libremsonic.server.api_objects import Child, AlbumWithSongsID3
Album = Union[Child, AlbumWithSongsID3]
class AlbumsPanel(Gtk.ScrolledWindow):
class AlbumsPanel(Gtk.Box):
__gsignals__ = {
'song-clicked': (
GObject.SignalFlags.RUN_FIRST,
@@ -23,17 +23,172 @@ class AlbumsPanel(Gtk.ScrolledWindow):
),
}
currently_active_sort: str = 'random'
currently_active_alphabetical_sort: str = 'name'
currently_active_genre: str = 'Rock'
currently_active_from_year: int = 2010
currently_active_to_year: int = 2020
def __init__(self):
Gtk.ScrolledWindow.__init__(self)
self.child = AlbumsGrid()
self.child.connect(
super().__init__(orientation=Gtk.Orientation.VERTICAL)
actionbar = Gtk.ActionBar()
# Sort by
actionbar.add(Gtk.Label(label='Sort by:'))
self.sort_type_combo = self.make_combobox(
(
('random', 'Random'),
('newest', 'Most recently added'),
('highest', 'Highest rated'),
('frequent', 'Most played'),
('recent', 'Most recently played'),
('alphabetical', 'Alphabetically'),
('starred', 'Starred only'),
('byYear', 'Year'),
('byGenre', 'Genre'),
),
self.on_type_combo_changed,
)
actionbar.pack_start(self.sort_type_combo)
# Alphabetically how?
self.alphabetical_type_combo = self.make_combobox(
(
('name', 'by album name'),
('artist', 'by artist name'),
),
self.on_alphabetical_type_change,
)
actionbar.pack_start(self.alphabetical_type_combo)
# Alphabetically how?
self.genre_combo = self.make_combobox(
(),
self.on_genre_change,
)
actionbar.pack_start(self.genre_combo)
self.from_year_label = Gtk.Label(label='from')
actionbar.pack_start(self.from_year_label)
self.from_year_entry = Gtk.Entry()
self.from_year_entry.connect('changed', self.on_year_changed)
actionbar.pack_start(self.from_year_entry)
self.to_year_label = Gtk.Label(label='to')
actionbar.pack_start(self.to_year_label)
self.to_year_entry = Gtk.Entry()
self.to_year_entry.connect('changed', self.on_year_changed)
actionbar.pack_start(self.to_year_entry)
refresh = util.button_with_icon('view-refresh')
refresh.connect('clicked', lambda *a: self.update(force=True))
actionbar.pack_end(refresh)
self.add(actionbar)
scrolled_window = Gtk.ScrolledWindow()
self.grid = AlbumsGrid()
self.grid.connect(
'song-clicked',
lambda _, song, queue: self.emit('song-clicked', song, queue),
)
self.add(self.child)
scrolled_window.add(self.grid)
self.add(scrolled_window)
def update(self, state: ApplicationState):
self.child.update(state)
def make_combobox(self, items, on_change):
store = Gtk.ListStore(str, str)
for item in items:
store.append(item)
combo = Gtk.ComboBox.new_with_model(store)
combo.set_id_column(0)
combo.connect('changed', on_change)
renderer_text = Gtk.CellRendererText()
combo.pack_start(renderer_text, True)
combo.add_attribute(renderer_text, 'text', 1)
return combo
def populate_genre_combo(self):
def get_genres_done(f):
model = self.genre_combo.get_model()
for genre in (f.result() or []):
model.append((genre.value, genre.value))
self.genre_combo.set_active_id(self.currently_active_genre)
genres_future = CacheManager.get_genres()
genres_future.add_done_callback(
lambda f: GLib.idle_add(get_genres_done, f))
def update(self, state: ApplicationState = None, force: bool = False):
# TODO store this in state
self.sort_type_combo.set_active_id(self.currently_active_sort)
self.alphabetical_type_combo.set_active_id(
self.currently_active_alphabetical_sort)
self.from_year_entry.set_text(str(self.currently_active_from_year))
self.to_year_entry.set_text(str(self.currently_active_to_year))
self.populate_genre_combo()
# Show/hide the combo boxes.
def show_if(sort_type, *elements):
for element in elements:
if self.currently_active_sort == sort_type:
element.show()
else:
element.hide()
show_if('alphabetical', self.alphabetical_type_combo)
show_if('byGenre', self.genre_combo)
show_if('byYear', self.from_year_label, self.from_year_entry)
show_if('byYear', self.to_year_label, self.to_year_entry)
self.grid.update(state=state, force=force)
def get_id(self, combo):
tree_iter = combo.get_active_iter()
if tree_iter is not None:
return combo.get_model()[tree_iter][0]
def on_type_combo_changed(self, combo):
self.currently_active_sort = self.get_id(combo)
if self.grid.type_ != self.currently_active_sort:
self.grid.update_params(type_=self.currently_active_sort)
self.update(force=True)
def on_alphabetical_type_change(self, combo):
self.currently_active_alphabetical_sort = self.get_id(combo)
if self.grid.alphabetical_type != self.currently_active_alphabetical_sort:
self.grid.update_params(
alphabetical_type=self.currently_active_alphabetical_sort)
self.update(force=True)
def on_genre_change(self, combo):
self.currently_active_genre = self.get_id(combo)
if self.grid.genre != self.currently_active_genre:
self.grid.update_params(genre=self.currently_active_genre)
self.update(force=True)
def on_year_changed(self, entry):
try:
year = int(entry.get_text())
except:
print('failed, should do something to prevent non-numeric input')
return
if self.to_year_entry == entry:
self.currently_active_to_year = year
if self.grid.to_year != self.currently_active_to_year:
self.grid.update_params(to_year=year)
self.update(force=True)
else:
self.currently_active_from_year = year
if self.grid.from_year != self.currently_active_from_year:
self.grid.update_params(from_year=year)
self.update(force=True)
class AlbumModel(GObject.Object):
@@ -47,6 +202,25 @@ class AlbumModel(GObject.Object):
class AlbumsGrid(CoverArtGrid):
"""Defines the albums panel."""
type_: str = 'random'
alphabetical_type: str = 'name'
from_year: int = 2010
to_year: int = 2020
genre: str = 'Rock'
def update_params(
self,
type_: str = None,
alphabetical_type: str = None,
from_year: int = None,
to_year: int = None,
genre: str = None,
):
self.type_ = type_ or self.type_
self.alphabetical_type = alphabetical_type or self.alphabetical_type
self.from_year = from_year or self.from_year
self.to_year = to_year or self.to_year
self.genre = genre or self.genre
# Override Methods
# =========================================================================
@@ -57,10 +231,21 @@ class AlbumsGrid(CoverArtGrid):
def get_info_text(self, item: AlbumModel) -> Optional[str]:
return util.dot_join(item.album.artist, item.album.year)
def get_model_list_future(self, before_download):
def get_model_list_future(self, before_download, force=False):
type_ = self.type_
if self.type_ == 'alphabetical':
type_ += {
'name': 'ByName',
'artist': 'ByArtist',
}[self.alphabetical_type]
return CacheManager.get_albums(
type_='random',
type_=type_,
to_year=self.to_year,
from_year=self.from_year,
genre=self.genre,
before_download=before_download,
force=force,
)
def create_model_from_element(self, album):

View File

@@ -195,7 +195,7 @@ class AlbumWithSongs(Gtk.Box):
def deselect_all(self):
self.album_songs.get_selection().unselect_all()
def update(self):
def update(self, force=False):
self.update_album_songs(self.album.id)
@util.async_callback(

View File

@@ -90,14 +90,15 @@ class CoverArtGrid(Gtk.ScrolledWindow):
self.add(overlay)
def update(self, state: ApplicationState = None):
self.update_grid()
def update(self, state: ApplicationState = None, force: bool = False):
self.update_grid(force=force)
# Update the detail panel.
children = self.detail_box_inner.get_children()
if len(children) > 0 and hasattr(children[0], 'update'):
children[0].update()
children[0].update(force=force)
def update_grid(self):
def update_grid(self, force=False):
def start_loading():
self.spinner.show()
@@ -115,18 +116,26 @@ class CoverArtGrid(Gtk.ScrolledWindow):
if selection:
self.selected_list_store_index = selection[0].get_index()
if force:
self.selected_list_store_index = None
old_len = len(self.list_store)
self.list_store.remove_all()
for el in result:
for el in (result or []):
self.list_store.append(self.create_model_from_element(el))
new_len = len(self.list_store)
# Only force if there's a length change.
# TODO, this doesn't handle when something is edited.
self.reflow_grids(force_reload_from_master=old_len != new_len)
self.reflow_grids(
force_reload_from_master=(old_len != new_len or force))
stop_loading()
future = self.get_model_list_future(before_download=start_loading)
print('update grid')
future = self.get_model_list_future(
before_download=start_loading,
force=force,
)
future.add_done_callback(lambda f: GLib.idle_add(future_done, f))
def create_widget(self, item):
@@ -234,7 +243,7 @@ class CoverArtGrid(Gtk.ScrolledWindow):
'get_info_text must be implemented by the inheritor of '
'CoverArtGrid.')
def get_model_list_future(self, before_download):
def get_model_list_future(self, before_download, force=False):
raise NotImplementedError(
'get_model_list_future must be implemented by the inheritor of '
'CoverArtGrid.')