mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(library): local lossless index, filters, and conserve dedicated page (#871)
* feat(library): local lossless index, filters, and conserve dedicated page Add SQLite-backed lossless album browse and advanced-search filtering, wire All Albums and artist/album lossless drill-down mode, and hide the standalone /lossless-albums nav entry from sidebar visibility settings (conserved route, default off). * docs(release): note lossless local index in CHANGELOG and credits (PR #871)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { search, searchSongsPaged } from '../../api/subsonicSearch';
|
||||
import type { SearchResults, SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { libraryAdvancedSearch } from '../../api/library';
|
||||
import { libraryAdvancedSearch, libraryGetArtistLosslessBrowse, libraryListLosslessAlbums } from '../../api/library';
|
||||
import { libraryScopeForServer } from '../../api/subsonicClient';
|
||||
import {
|
||||
LIVE_SEARCH_DEBOUNCE_NETWORK_MS,
|
||||
@@ -364,6 +364,52 @@ export async function runLocalRandomSongs(
|
||||
}
|
||||
}
|
||||
|
||||
/** Paginated lossless albums from the local index. Returns null when unavailable. */
|
||||
export async function runLocalLosslessAlbums(
|
||||
serverId: string | null | undefined,
|
||||
limit: number,
|
||||
offset: number,
|
||||
): Promise<{ albums: SubsonicAlbum[]; hasMore: boolean } | null> {
|
||||
if (!serverId || !(await libraryIsReady(serverId))) return null;
|
||||
try {
|
||||
const resp = await libraryListLosslessAlbums({
|
||||
serverId,
|
||||
libraryScope: libraryScopeForServer(serverId) ?? undefined,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
if (resp.source !== 'local') return null;
|
||||
return {
|
||||
albums: resp.albums.map(albumToAlbum),
|
||||
hasMore: resp.hasMore,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lossless albums + tracks for one artist. Returns null when the index is unavailable. */
|
||||
export async function runLocalArtistLosslessBrowse(
|
||||
serverId: string | null | undefined,
|
||||
artistId: string,
|
||||
): Promise<{ albums: SubsonicAlbum[]; songs: SubsonicSong[] } | null> {
|
||||
if (!serverId || !artistId || !(await libraryIsReady(serverId))) return null;
|
||||
try {
|
||||
const resp = await libraryGetArtistLosslessBrowse({
|
||||
serverId,
|
||||
artistId,
|
||||
libraryScope: libraryScopeForServer(serverId) ?? undefined,
|
||||
});
|
||||
if (resp.source !== 'local') return null;
|
||||
return {
|
||||
albums: resp.albums.map(albumToAlbum),
|
||||
songs: resp.tracks.map(trackToSong),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Random album sample from the local `album` table — SQLite `ORDER BY RANDOM() LIMIT N`.
|
||||
* Returns null when the index is unavailable (caller falls back to the network).
|
||||
@@ -397,6 +443,7 @@ export async function runLocalAlbumBrowsePage(
|
||||
offset: number,
|
||||
pageSize: number,
|
||||
yearFilter?: { from: number; to: number },
|
||||
losslessOnly?: boolean,
|
||||
): Promise<SubsonicAlbum[] | null> {
|
||||
if (!serverId || !(await libraryIsReady(serverId))) return null;
|
||||
const filters: LibraryFilterClause[] = [];
|
||||
@@ -408,6 +455,9 @@ export async function runLocalAlbumBrowsePage(
|
||||
valueTo: yearFilter.to,
|
||||
});
|
||||
}
|
||||
if (losslessOnly) {
|
||||
filters.push({ field: 'lossless', op: 'is_true' });
|
||||
}
|
||||
try {
|
||||
const resp = await libraryAdvancedSearch({
|
||||
serverId,
|
||||
@@ -436,22 +486,25 @@ export async function runLocalAlbumsByGenres(
|
||||
genres: string[],
|
||||
sort: AlbumBrowseSort,
|
||||
limitPerGenre = GENRE_ALBUM_FETCH_LIMIT,
|
||||
losslessOnly?: boolean,
|
||||
): Promise<SubsonicAlbum[] | null> {
|
||||
if (!serverId || !(await libraryIsReady(serverId)) || genres.length === 0) return null;
|
||||
try {
|
||||
const pages = await Promise.all(
|
||||
genres.map(genre =>
|
||||
libraryAdvancedSearch({
|
||||
genres.map(genre => {
|
||||
const filters: LibraryFilterClause[] = [{ field: 'genre', op: 'eq', value: genre }];
|
||||
if (losslessOnly) filters.push({ field: 'lossless', op: 'is_true' });
|
||||
return libraryAdvancedSearch({
|
||||
serverId,
|
||||
libraryScope: libraryScopeForServer(serverId) ?? undefined,
|
||||
entityTypes: ['album'],
|
||||
filters: [{ field: 'genre', op: 'eq', value: genre }],
|
||||
filters,
|
||||
sort: albumSortClauses(sort),
|
||||
limit: limitPerGenre,
|
||||
offset: 0,
|
||||
skipTotals: true,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (pages.some(p => p.source !== 'local')) return null;
|
||||
const merged = dedupeById(pages.flatMap(p => p.albums.map(albumToAlbum)));
|
||||
|
||||
Reference in New Issue
Block a user