Files
Psychotoxical-psysonic/src/features/offline/utils/offlinePlaylistBrowse.ts
T
Psychotoxical 2756cb7698 refactor(lib): relocate the local-library subsystem utils/library -> lib/library
utils/library is the local-index query/browse/sync engine -- feature-free shared
infra consumed by the album, search, genre, artist and offline features. With
its last feature edges removed (queue resolver, shuffleArray, albumBrowseCatalogChunk,
genreBrowsePlayback all relocated earlier this branch), the whole directory is
pure infra and belongs under lib/.

Whole-directory move (58 files, 76 consumers rewritten) via resolver-based
rewrite: cross-cluster relatives absolutized to @/, intra-cluster siblings kept
relative. utils/library is gone; lib/library imports zero @/features. Three
stale doc comments in the album/search/artist barrels repointed.

tsc 0, lint 0, full suite 2353/2353, production build OK.
2026-06-30 17:06:03 +02:00

110 lines
4.2 KiB
TypeScript

import { libraryGetTracksBatchChunked } from '@/lib/api/library';
import type { SubsonicPlaylist, SubsonicSong } from '@/lib/api/subsonicTypes';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import type { PinnedGroup } from '@/store/localPlaybackStore';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { trackToSong } from '@/lib/library/advancedSearchLocal';
import { isManualOfflinePlaylist } from '@/features/offline/utils/pinnedOfflineSync';
import {
hasLocalLibraryBytes,
indexKeyBelongsToServer,
} from '@/store/localPlaybackResolve';
import { resolveOfflineAlbumMeta } from '@/features/offline/utils/offlineLibraryHelpers';
function listPlaylistPinnedGroupsForServer(serverId: string): PinnedGroup[] {
return useLocalPlaybackStore.getState()
.listPinnedGroups()
.filter(g => g.pinSource.kind === 'playlist' && indexKeyBelongsToServer(g.serverIndexKey, serverId));
}
function orderedPlayableTrackIds(
playlistId: string,
serverId: string,
group: PinnedGroup,
): string[] {
const meta = resolveOfflineAlbumMeta(playlistId, serverId);
const ordered = meta?.trackIds?.length ? meta.trackIds : group.trackIds;
return ordered.filter(tid => hasLocalLibraryBytes(tid, serverId));
}
/** Cached regular playlists with on-disk bytes for the active server. */
export function playlistsOfflineBrowseEnabled(serverId: string | null | undefined): boolean {
if (!serverId) return false;
if (!useLibraryIndexStore.getState().isIndexEnabled(serverId)) return false;
return listPlaylistPinnedGroupsForServer(serverId).some(g => {
if (!isManualOfflinePlaylist(g.pinSource.sourceId, serverId, g.pinSource.displayName)) {
return false;
}
return orderedPlayableTrackIds(g.pinSource.sourceId, serverId, g).length > 0;
});
}
export async function fetchOfflineBrowsablePlaylists(serverId: string): Promise<SubsonicPlaylist[]> {
const groups = listPlaylistPinnedGroupsForServer(serverId)
.filter(g => isManualOfflinePlaylist(g.pinSource.sourceId, serverId, g.pinSource.displayName));
const playlists: SubsonicPlaylist[] = [];
for (const group of groups) {
const playlistId = group.pinSource.sourceId;
const trackIds = orderedPlayableTrackIds(playlistId, serverId, group);
if (trackIds.length === 0) continue;
const meta = resolveOfflineAlbumMeta(playlistId, serverId);
const refs = trackIds.map(trackId => ({ serverId, trackId }));
const dtos = await libraryGetTracksBatchChunked(refs);
const byId = new Map(dtos.map(d => [d.id, d]));
let duration = 0;
for (const trackId of trackIds) {
duration += byId.get(trackId)?.durationSec ?? 0;
}
playlists.push({
id: playlistId,
name: group.pinSource.displayName ?? meta?.name ?? playlistId,
songCount: trackIds.length,
duration,
created: '',
changed: '',
coverArt: meta?.coverArt,
});
}
return playlists.sort((a, b) => a.name.localeCompare(b.name));
}
export async function loadOfflineBrowsablePlaylist(
playlistId: string,
serverId: string,
): Promise<{ playlist: SubsonicPlaylist; songs: SubsonicSong[] } | null> {
const group = listPlaylistPinnedGroupsForServer(serverId)
.find(g => g.pinSource.sourceId === playlistId);
if (!group) return null;
if (!isManualOfflinePlaylist(playlistId, serverId, group.pinSource.displayName)) return null;
const trackIds = orderedPlayableTrackIds(playlistId, serverId, group);
if (trackIds.length === 0) return null;
const meta = resolveOfflineAlbumMeta(playlistId, serverId);
const refs = trackIds.map(trackId => ({ serverId, trackId }));
const dtos = await libraryGetTracksBatchChunked(refs);
const byId = new Map(dtos.map(d => [d.id, d]));
const songs = trackIds
.map(id => byId.get(id))
.filter((dto): dto is NonNullable<typeof dto> => !!dto)
.map(dto => ({ ...trackToSong(dto), serverId }));
const duration = songs.reduce((sum, song) => sum + (song.duration ?? 0), 0);
return {
playlist: {
id: playlistId,
name: group.pinSource.displayName ?? meta?.name ?? playlistId,
songCount: songs.length,
duration,
created: '',
changed: '',
coverArt: meta?.coverArt,
},
songs,
};
}