diff --git a/.attachments/albums-screen-random.png b/.attachments/albums-screen-random.png new file mode 100644 index 0000000..1e9cfbb Binary files /dev/null and b/.attachments/albums-screen-random.png differ diff --git a/Navidrome.py b/Navidrome.py index 5c0c4ac..ea0a837 100644 --- a/Navidrome.py +++ b/Navidrome.py @@ -57,4 +57,26 @@ class NavidromeAPI: return [SubSonic.Song(song_d) for song_d in response.json()] + def getAlbums(self, end: int, start: int, order: str = 'ACS', sort: str = 'name', name: str | None = None, seed: float | None = None) -> list[SubSonic.Album]: + url = self.base_url + '/api/album?' + + payload = { + "_end": end, + "_order": order, + "_sort" : sort, + "_start": start, + "seed": 0.0 + } + + if seed: + payload['seed'] = seed + + if name: + payload['name'] = name + + response = requests.get(url + self._encodePayload(payload), headers=self.auth_header) + + print(response.content) + + return [SubSonic.Album(album_d) for album_d in response.json()] diff --git a/README.md b/README.md index a3296f6..873a2b4 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,7 @@ Artists Page View of favorite songs ![View of favorited songs](.attachments/songs-screen-favourites.png) + +View of some random albums + +![View of some random albums](.attachments/albums-screen-random.png) diff --git a/SubSonic.py b/SubSonic.py index bec6de4..b298d5e 100644 --- a/SubSonic.py +++ b/SubSonic.py @@ -32,7 +32,7 @@ class Album: name: str album: str artist: str - coverArtId: str + coverArtId: str | None duration: int artistId: str songCount: int @@ -42,11 +42,11 @@ class Album: def __init__(self, album_info: dict): self.id = album_info['id'] - self.title = album_info['title'] self.name = album_info['name'] - self.album = album_info['album'] + self.title = album_info['title'] if 'title' in album_info else self.name + self.album = album_info['album'] if 'album' in album_info else self.name self.artist = album_info['artist'] - self.coverArtId = album_info['coverArt'] + self.coverArtId = album_info['coverArt'] if 'coverArt' in album_info else None self.duration = int(album_info['duration']) self.artistId = album_info['artistId'] self.songCount = int(album_info['songCount']) @@ -448,5 +448,3 @@ class SubpyConn: return random_songs - - diff --git a/app.py b/app.py index 51494b3..1441aee 100644 --- a/app.py +++ b/app.py @@ -561,11 +561,24 @@ class ArtistsGUI(Screen): artist_panel.set_artist(filtered_artists[self.artists_displayed]) self.artists_displayed += 1 - - class Albums(Screen): - albums: list[SubSonic.Album] + BINDINGS = [ + ("a", "page_backward()", "Prev. Page"), + ("d", "page_forward()", "Next Page") + ] + + modes: list[str] = ['All', 'Random', 'Recently Added'] + # list[page number, albums] + albums: list[tuple[int, list[SubSonic.Album]]] = [(0, []) for n in range(len(modes))] + mode: int = 0 + page: list[int] = [0 for n in range(len(modes))] + DATATABLE_SIZE: int + seed: float = random.random() + last_mode: int = 0 + + SPACE_NEEDED: int = 12 + search_value: str = '' def compose(self) -> ComposeResult: yield Header(icon='') @@ -575,21 +588,96 @@ class Albums(Screen): yield Label(Text("* F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb") yield Label(Text("F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb") yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb") - with Vertical(id="albumContainer"): - yield Input(placeholder="Search", id="albumSearchBar") - yield DataTable(id="albumList") + + with Horizontal(id='albumScreen'): + yield ListView( + *[ListItem(Label(mode_name)) for mode_name in self.modes], + id='albumModeSelectorList' + ) + + with Vertical(id="albumContainer"): + yield Input(placeholder="Search", id="albumSearchBar") + yield DataTable(id="albumList") + yield Label(id="pager") + yield Footer() def on_mount(self) -> None: app.sub_title = "Albums" - self.albums = conn.getAllAlbums() - albumTable = self.query_one(DataTable) + dt = self.query_one(DataTable) - albumTable.cursor_type = 'row' - albumTable.add_columns("Title", "Artist", "Songs", "Plays", "Year", "Time") + dt.cursor_type = 'row' + dt.add_columns("Title", "Artist", "Songs", "Plays", "Time") - for album in self.albums: - albumTable.add_row(album.title, album.artist, album.songCount, album.playCount, album.year, album.duration, key=album.id) + self.DATATABLE_SIZE = self.size.height - self.SPACE_NEEDED + self._update_datatable() + + self._update_pager() + + @on(events.Resize) + def on_resize(self) -> None: + self.DATATABLE_SIZE = self.size.height - self.SPACE_NEEDED + self._update_datatable() + + def action_page_forward(self) -> None: + self.page[self.mode] += 1 + self._update_datatable() + + def action_page_backward(self) -> None: + if self.page[self.mode] - 1 < 0: + return + + self.page[self.mode] -= 1 + self._update_datatable() + + def get_current_page(self) -> int: + return self.page[self.mode] + + def _get_offset(self) -> int: + return (self.get_current_page()) * self.DATATABLE_SIZE + + def _update_pager(self) -> None: + pager = self.query_one("#pager", Label) + pager.update(f'Page {self.page[self.mode] + 1}') + + def _update_datatable(self, force_update: bool = False) -> None: + dt = self.query_one(DataTable) + + space = self.DATATABLE_SIZE + + offset = self._get_offset() + current_page: int = self.get_current_page() + + page_n, albums = self.albums[self.mode] + + if self.page[self.mode] == 0 and self.mode == 1 and self.last_mode != 1: + self.seed = random.random() + albums = [] + + if page_n != current_page or not albums or len(albums) < space or force_update or current_page == 1: + + if self.mode == 0: + albums = app.navidrome.getAlbums(offset + space, offset, name=self.search_value) + elif self.mode == 1: + albums = app.navidrome.getAlbums(offset + space, offset, sort='random', seed=self.seed, name=self.search_value) + elif self.mode == 2: + albums = app.navidrome.getAlbums(offset + space, offset, order='DESC', sort='recently_added', name=self.search_value) + + self.albums[self.mode] = (current_page, albums) + + dt.clear() + + for album in albums[:space]: + dt.add_row(album.title, album.artist, album.songCount, album.playCount, album.duration, key=album.id) + + self.last_mode = self.mode + + self._update_pager() + + @on(ListView.Selected) + def mode_selected(self, e: ListView.Selected) -> None: + self.mode = e.index + self._update_datatable() @on(events.ScreenResume) def on_screen_resume(self, event: events.ScreenResume) -> None: @@ -602,14 +690,13 @@ class Albums(Screen): @on(Input.Submitted) def search_submit(self, event: Input.Submitted) -> None: - search_value = event.value.lower() + self.search_value = event.value.lower() - album_table = self.query_one(DataTable) + if self.search_value: + self.page[self.mode] = 0 + + self._update_datatable(force_update=True) - album_table.clear() - for album in self.albums: - if search_value in album.title.lower() or search_value in album.artist.lower(): - album_table.add_row(album.title, album.artist, album.songCount, album.playCount, album.year, album.duration, key=album.id) class Playlists(Screen): @@ -829,63 +916,6 @@ class SongsScreen(Screen): for song in self.songs: dt.add_row(song.title, song.album, song.artist, song.playCount, song.duration) -class Favourites(Screen): - BINDINGS = [ - ('p', 'play()', 'Play Song'), - ('q', 'queue()', 'Queue Song'), - ('m', 'shuffle_mix()', 'Shuffle Play') - ] - - songs: list[SubSonic.Song] - selected_song_id: str | None = None - - def compose(self) -> ComposeResult: - yield Header(icon="") - with Horizontal(id="headerbs"): - yield Label(Text("F1 ", style="bold orange").append_text(Text("Player", style="white")), classes="headerb") - yield Label(Text("F2 ", style="bold orange").append_text(Text("Artists", style="white")), classes="headerb") - yield Label(Text("F3 ", style="bold orange").append_text(Text("Albums", style="white")), classes="headerb") - yield Label(Text("* F4 ", style="bold orange").append_text(Text("Songs", style="white")), classes="headerb") - yield Label(Text("F5 ", style="bold orange").append_text(Text("Playlists", style="white")), classes="headerb") - yield DataTable() - yield Footer() - - def on_mount(self) -> None: - app.sub_title = "Favourites" - self.songs = conn.getStarredSongs() - songTable = self.query_one(DataTable) - - songTable.cursor_type = "row" - songTable.add_columns("Title", "Album", "Artist", "Plays", "Duration", "Bitrate") - - for song in self.songs: - songTable.add_row(song.title, song.album, song.artist, song.playCount, song.duration, song.bitRate, key=song.id) - - @on(DataTable.RowHighlighted) - def row_highlighted(self, event: DataTable.RowHighlighted) -> None: - self.selected_song_id = event.row_key.value - - def action_play(self) -> None: - song_index = self._find_selected_song() - app.play_song(self.songs[song_index]) - app.song_queue = self.songs[song_index + 1:] - - def action_queue(self) -> None: - song_index = self._find_selected_song() - app.queue_song(self.songs[song_index]) - - # Returns index of selected song - def _find_selected_song(self) -> int: - for i, song in enumerate(self.songs): - if song.id == self.selected_song_id: - return i - - def action_shuffle_mix(self) -> None: - q = self.songs.copy() - random.shuffle(q) - app.play_song(q.pop(0)) - app.song_queue = q - class CoverArtPixels(Container): cover = reactive[Image.Image | None](None) diff --git a/main.tcss b/main.tcss index c3f24c4..d7d2770 100644 --- a/main.tcss +++ b/main.tcss @@ -183,6 +183,23 @@ QueueScreen { width: 1fr; } -# SongsScreen { -# background: red; -# } +#albumModeSelectorList { + height: 100%; + width: 0.2fr; +} + +#albumModeSelectorList Label { + padding: 1 2; +} + +#albumSearchBar { + margin: 1 0; +} + +#albumList { + min-height: 1fr; + height: 1fr; + # border: white; + margin: 1 1; +} +