mirror of
https://github.com/leinelissen/jellyfin-audio-player.git
synced 2026-09-02 21:03:11 +03:00
Add music fetchers and database hooks to bridge Jellyfin API with database
Co-authored-by: leinelissen <10154841+leinelissen@users.noreply.github.com>
This commit is contained in:
co-authored by
leinelissen
parent
e34dfc3780
commit
27a4c79fe8
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Bridge functions to fetch from Jellyfin API and store in database
|
||||
* These replace the Redux thunks with direct database operations
|
||||
*/
|
||||
|
||||
import { retrieveAllAlbums, retrieveRecentAlbums, retrieveAlbumTracks, retrieveAlbum, retrieveSimilarAlbums } from '@/utility/JellyfinApi/album';
|
||||
import { retrieveAllPlaylists, retrievePlaylistTracks, retrieveInstantMixByTrackId } from '@/utility/JellyfinApi/playlist';
|
||||
import { retrieveAllArtists } from '@/utility/JellyfinApi/artist';
|
||||
import { searchItem } from '@/utility/JellyfinApi/search';
|
||||
import { retrieveTrackLyrics } from '@/utility/JellyfinApi/lyrics';
|
||||
import { retrieveTrackCodecMetadata } from '@/utility/JellyfinApi/track';
|
||||
import { getActiveSource } from '@/store/settings/db';
|
||||
import * as musicDb from './db';
|
||||
import type { Album, AlbumTrack, MusicArtist, Playlist } from './types';
|
||||
|
||||
/**
|
||||
* Fetch all albums from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreAllAlbums() {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const albums = await retrieveAllAlbums();
|
||||
await musicDb.upsertAlbums(source.id, albums);
|
||||
return albums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch recent albums from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreRecentAlbums() {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const albums = await retrieveRecentAlbums();
|
||||
await musicDb.upsertAlbums(source.id, albums);
|
||||
return albums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single album from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreAlbum(albumId: string) {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const album = await retrieveAlbum(albumId);
|
||||
await musicDb.upsertAlbum(source.id, album);
|
||||
return album;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch similar albums from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreSimilarAlbums(albumId: string) {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const similarAlbums = await retrieveSimilarAlbums(albumId);
|
||||
await musicDb.upsertAlbums(source.id, similarAlbums);
|
||||
await musicDb.setSimilarAlbums(source.id, albumId, similarAlbums.map(a => a.Id));
|
||||
return similarAlbums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch tracks by album from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreTracksByAlbum(albumId: string) {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const tracks = await retrieveAlbumTracks(albumId);
|
||||
await musicDb.upsertTracks(source.id, tracks);
|
||||
|
||||
// Fetch codec metadata and lyrics for tracks
|
||||
await Promise.all(tracks.map(async (track) => {
|
||||
if (track.HasLyrics) {
|
||||
try {
|
||||
const lyrics = await retrieveTrackLyrics(track.Id);
|
||||
track.Lyrics = lyrics;
|
||||
await musicDb.upsertTrack(source.id, track);
|
||||
} catch (e) {
|
||||
console.error('Error fetching lyrics for track', track.Id, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const codec = await retrieveTrackCodecMetadata(track.Id);
|
||||
await musicDb.updateTrackCodec(track.Id, codec);
|
||||
} catch (e) {
|
||||
console.error('Error fetching codec for track', track.Id, e);
|
||||
}
|
||||
}));
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all artists from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreAllArtists() {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const artists = await retrieveAllArtists();
|
||||
await musicDb.upsertArtists(source.id, artists);
|
||||
return artists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all playlists from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreAllPlaylists() {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const playlists = await retrieveAllPlaylists();
|
||||
await musicDb.upsertPlaylists(source.id, playlists);
|
||||
return playlists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch tracks by playlist from Jellyfin and store in database
|
||||
*/
|
||||
export async function fetchAndStoreTracksByPlaylist(playlistId: string) {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const tracks = await retrievePlaylistTracks(playlistId);
|
||||
await musicDb.upsertTracks(source.id, tracks);
|
||||
await musicDb.setPlaylistTracks(source.id, playlistId, tracks.map(t => t.Id));
|
||||
|
||||
// Fetch codec metadata and lyrics for tracks
|
||||
await Promise.all(tracks.map(async (track) => {
|
||||
if (track.HasLyrics) {
|
||||
try {
|
||||
const lyrics = await retrieveTrackLyrics(track.Id);
|
||||
track.Lyrics = lyrics;
|
||||
await musicDb.upsertTrack(source.id, track);
|
||||
} catch (e) {
|
||||
console.error('Error fetching lyrics for track', track.Id, e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const codec = await retrieveTrackCodecMetadata(track.Id);
|
||||
await musicDb.updateTrackCodec(track.Id, codec);
|
||||
} catch (e) {
|
||||
console.error('Error fetching codec for track', track.Id, e);
|
||||
}
|
||||
}));
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Jellyfin and store results in database
|
||||
*/
|
||||
export async function searchAndStore(term: string) {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const results = await searchItem(term);
|
||||
|
||||
// Separate results by type
|
||||
const albums = results.filter(item => item.Type === 'MusicAlbum') as Album[];
|
||||
const tracks = results.filter(item => item.Type === 'Audio') as AlbumTrack[];
|
||||
const artists = results.filter(item => item.Type === 'MusicArtist') as MusicArtist[];
|
||||
const playlists = results.filter(item => item.Type === 'Playlist') as Playlist[];
|
||||
|
||||
// Store in database
|
||||
await Promise.all([
|
||||
musicDb.upsertAlbums(source.id, albums),
|
||||
musicDb.upsertTracks(source.id, tracks),
|
||||
musicDb.upsertArtists(source.id, artists),
|
||||
musicDb.upsertPlaylists(source.id, playlists),
|
||||
]);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch instant mix by track and store in database
|
||||
*/
|
||||
export async function fetchAndStoreInstantMixByTrack(trackId: string) {
|
||||
const source = await getActiveSource();
|
||||
if (!source) throw new Error('No active source');
|
||||
|
||||
const tracks = await retrieveInstantMixByTrackId(trackId);
|
||||
await musicDb.upsertTracks(source.id, tracks);
|
||||
return tracks;
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Database-backed hooks for music data
|
||||
* These replace Redux selectors with live database queries
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useLiveQuery } from '@/store/db/live-queries';
|
||||
import { db } from '@/store/db';
|
||||
import { albums } from '@/store/db/schema/albums';
|
||||
import { artists } from '@/store/db/schema/artists';
|
||||
import { tracks } from '@/store/db/schema/tracks';
|
||||
import { playlists } from '@/store/db/schema/playlists';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { ALPHABET_LETTERS } from '@/CONSTANTS';
|
||||
import type { SectionListData } from 'react-native';
|
||||
import type { Album, AlbumTrack, MusicArtist, Playlist } from './types';
|
||||
|
||||
/**
|
||||
* Get all albums for a source
|
||||
*/
|
||||
export function useAlbums(sourceId: string) {
|
||||
const { data, error } = useLiveQuery(
|
||||
sourceId ? db.select().from(albums).where(eq(albums.sourceId, sourceId)) : null
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const albumsMap: Record<string, Album> = {};
|
||||
const ids: string[] = [];
|
||||
|
||||
(data || []).forEach(album => {
|
||||
const enriched = enrichAlbum(album);
|
||||
albumsMap[enriched.Id] = enriched;
|
||||
ids.push(enriched.Id);
|
||||
});
|
||||
|
||||
return { albums: albumsMap, ids, error, isLoading: false };
|
||||
}, [data, error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent albums (sorted by date created)
|
||||
*/
|
||||
export function useRecentAlbums(sourceId: string, amount: number = 24) {
|
||||
const { data, error } = useLiveQuery(
|
||||
sourceId
|
||||
? db.select().from(albums).where(eq(albums.sourceId, sourceId)).orderBy(desc(albums.dateCreated)).limit(amount)
|
||||
: null
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const albumsMap: Record<string, Album> = {};
|
||||
const ids: string[] = [];
|
||||
|
||||
(data || []).forEach(album => {
|
||||
const enriched = enrichAlbum(album);
|
||||
albumsMap[enriched.Id] = enriched;
|
||||
ids.push(enriched.Id);
|
||||
});
|
||||
|
||||
return { albums: albumsMap, ids, error };
|
||||
}, [data, error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get albums sorted by alphabet with sections
|
||||
*/
|
||||
export function useAlbumsByAlphabet(sourceId: string) {
|
||||
const { albums: albumsMap, ids } = useAlbums(sourceId);
|
||||
|
||||
return useMemo(() => {
|
||||
// Sort by album artist
|
||||
const sorted = [...ids].sort((a, b) => {
|
||||
const albumA = albumsMap[a];
|
||||
const albumB = albumsMap[b];
|
||||
if ((!albumA && !albumB) || (!albumA?.AlbumArtist && !albumB?.AlbumArtist)) {
|
||||
return 0;
|
||||
} else if (!albumA || !albumA.AlbumArtist) {
|
||||
return 1;
|
||||
} else if (!albumB || !albumB.AlbumArtist) {
|
||||
return -1;
|
||||
}
|
||||
return albumA.AlbumArtist.localeCompare(albumB.AlbumArtist);
|
||||
});
|
||||
|
||||
// Split into alphabet sections
|
||||
const sections: SectionListData<string[]>[] = ALPHABET_LETTERS.split('').map((l) => ({ label: l, data: [[]] }));
|
||||
|
||||
sorted.forEach((id) => {
|
||||
const album = albumsMap[id];
|
||||
const letter = album?.AlbumArtist?.toUpperCase().charAt(0);
|
||||
const index = letter ? ALPHABET_LETTERS.indexOf(letter) : 26;
|
||||
|
||||
const section = sections[index >= 0 ? index : 26];
|
||||
const row = section.data.length - 1;
|
||||
|
||||
section.data[row].push(id);
|
||||
|
||||
if (section.data[row].length >= 2) {
|
||||
(section.data as string[][]).push([]);
|
||||
}
|
||||
});
|
||||
|
||||
return sections;
|
||||
}, [albumsMap, ids]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all artists for a source
|
||||
*/
|
||||
export function useArtists(sourceId: string) {
|
||||
const { data, error } = useLiveQuery(
|
||||
sourceId ? db.select().from(artists).where(eq(artists.sourceId, sourceId)) : null
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const artistsMap: Record<string, MusicArtist> = {};
|
||||
const ids: string[] = [];
|
||||
|
||||
(data || []).forEach(artist => {
|
||||
const enriched = enrichArtist(artist);
|
||||
artistsMap[enriched.Id] = enriched;
|
||||
ids.push(enriched.Id);
|
||||
});
|
||||
|
||||
return { artists: artistsMap, ids, error };
|
||||
}, [data, error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artists sorted by alphabet with sections
|
||||
*/
|
||||
export function useArtistsByAlphabet(sourceId: string) {
|
||||
const { artists: artistsMap } = useArtists(sourceId);
|
||||
|
||||
return useMemo(() => {
|
||||
const artistsList = Object.values(artistsMap);
|
||||
const sections: SectionListData<MusicArtist>[] = ALPHABET_LETTERS.split('').map((l) => ({ label: l, data: [] }));
|
||||
|
||||
artistsList.forEach((artist) => {
|
||||
const letter = artist.Name.toUpperCase().charAt(0);
|
||||
const index = letter ? ALPHABET_LETTERS.indexOf(letter) : 26;
|
||||
const section = sections[index >= 0 ? index : 26];
|
||||
(section.data as MusicArtist[]).push(artist);
|
||||
});
|
||||
|
||||
return sections;
|
||||
}, [artistsMap]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all playlists for a source
|
||||
*/
|
||||
export function usePlaylists(sourceId: string) {
|
||||
const { data, error } = useLiveQuery(
|
||||
sourceId ? db.select().from(playlists).where(eq(playlists.sourceId, sourceId)) : null
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const playlistsMap: Record<string, Playlist> = {};
|
||||
const ids: string[] = [];
|
||||
|
||||
(data || []).forEach(playlist => {
|
||||
const enriched = enrichPlaylist(playlist);
|
||||
playlistsMap[enriched.Id] = enriched;
|
||||
ids.push(enriched.Id);
|
||||
});
|
||||
|
||||
return { playlists: playlistsMap, ids, error };
|
||||
}, [data, error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracks by album
|
||||
*/
|
||||
export function useTracksByAlbum(albumId: string) {
|
||||
const { data, error } = useLiveQuery(
|
||||
albumId ? db.select().from(tracks).where(eq(tracks.albumId, albumId)) : null
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const tracksMap: Record<string, AlbumTrack> = {};
|
||||
const ids: string[] = [];
|
||||
|
||||
(data || []).forEach(track => {
|
||||
const enriched = enrichTrack(track);
|
||||
tracksMap[enriched.Id] = enriched;
|
||||
ids.push(enriched.Id);
|
||||
});
|
||||
|
||||
return { tracks: tracksMap, ids, error };
|
||||
}, [data, error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tracks for a source
|
||||
*/
|
||||
export function useTracks(sourceId: string) {
|
||||
const { data, error } = useLiveQuery(
|
||||
sourceId ? db.select().from(tracks).where(eq(tracks.sourceId, sourceId)) : null
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
const tracksMap: Record<string, AlbumTrack> = {};
|
||||
const ids: string[] = [];
|
||||
|
||||
(data || []).forEach(track => {
|
||||
const enriched = enrichTrack(track);
|
||||
tracksMap[enriched.Id] = enriched;
|
||||
ids.push(enriched.Id);
|
||||
});
|
||||
|
||||
return { tracks: tracksMap, ids, error };
|
||||
}, [data, error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions to enrich database rows with full type information
|
||||
*/
|
||||
|
||||
function enrichAlbum(dbAlbum: any): Album {
|
||||
const metadata = dbAlbum.metadataJson ? JSON.parse(dbAlbum.metadataJson) : {};
|
||||
return {
|
||||
Id: dbAlbum.id,
|
||||
Name: dbAlbum.name,
|
||||
ProductionYear: dbAlbum.productionYear,
|
||||
IsFolder: dbAlbum.isFolder,
|
||||
AlbumArtist: dbAlbum.albumArtist,
|
||||
DateCreated: dbAlbum.dateCreated ? new Date(dbAlbum.dateCreated).toISOString() : new Date().toISOString(),
|
||||
lastRefreshed: dbAlbum.lastRefreshed,
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
|
||||
function enrichArtist(dbArtist: any): MusicArtist {
|
||||
const metadata = dbArtist.metadataJson ? JSON.parse(dbArtist.metadataJson) : {};
|
||||
return {
|
||||
Id: dbArtist.id,
|
||||
Name: dbArtist.name,
|
||||
IsFolder: dbArtist.isFolder,
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
|
||||
function enrichTrack(dbTrack: any): AlbumTrack {
|
||||
const metadata = dbTrack.metadataJson ? JSON.parse(dbTrack.metadataJson) : {};
|
||||
const lyrics = dbTrack.lyrics ? JSON.parse(dbTrack.lyrics) : undefined;
|
||||
|
||||
return {
|
||||
Id: dbTrack.id,
|
||||
Name: dbTrack.name,
|
||||
AlbumId: dbTrack.albumId,
|
||||
Album: dbTrack.album,
|
||||
AlbumArtist: dbTrack.albumArtist,
|
||||
ProductionYear: dbTrack.productionYear,
|
||||
IndexNumber: dbTrack.indexNumber,
|
||||
ParentIndexNumber: dbTrack.parentIndexNumber,
|
||||
HasLyrics: dbTrack.hasLyrics,
|
||||
RunTimeTicks: dbTrack.runTimeTicks,
|
||||
Lyrics: lyrics,
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
|
||||
function enrichPlaylist(dbPlaylist: any): Playlist {
|
||||
const metadata = dbPlaylist.metadataJson ? JSON.parse(dbPlaylist.metadataJson) : {};
|
||||
return {
|
||||
Id: dbPlaylist.id,
|
||||
Name: dbPlaylist.name,
|
||||
CanDelete: dbPlaylist.canDelete,
|
||||
ChildCount: dbPlaylist.childCount,
|
||||
lastRefreshed: dbPlaylist.lastRefreshed,
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user