fix: simplify hooks

This commit is contained in:
Lei Nelissen
2026-02-17 17:02:31 +01:00
parent 04f670df8c
commit 0ad8a78a5d
12 changed files with 62 additions and 564 deletions
+14 -32
View File
@@ -1,47 +1,29 @@
/**
* Database-backed hooks for albums
*/
import { useMemo } from 'react';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
import albums from './entity';
import { and, eq, desc } from 'drizzle-orm';
import { and, eq } from 'drizzle-orm';
export function useAlbums(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(albums).where(eq(albums.sourceId, sourceId))
: db.select().from(albums)
return useLiveQuery(
db.query.albums.findMany({
where: sourceId ? eq(albums.sourceId, sourceId) : undefined,
})
);
return useMemo(() => ({
data: (data || []),
error,
}), [data, error]);
}
export function useAlbum([sourceId, id]: [sourceId: string, id: string]) {
const { data, error } = useLiveQuery(
db.select()
.from(albums)
.where(and(eq(albums.sourceId, sourceId), eq(albums.id, id)))
.limit(1)
return useLiveQuery(
db.query.albums.findFirst({
where: and(eq(albums.sourceId, sourceId), eq(albums.id, id))
})
);
return useMemo(() => ({
data: data?.[0],
error,
}), [data, error]);
}
export function useRecentAlbums(limit: number = 24) {
const { data, error } = useLiveQuery(
db.select().from(albums).orderBy(desc(albums.dateCreated)).limit(limit)
return useLiveQuery(
db.query.albums.findMany({
orderBy: (album, { desc }) => [desc(album.dateCreated)],
limit,
})
);
return useMemo(() => ({
data: (data || []),
error,
}), [data, error]);
}
+8 -24
View File
@@ -1,36 +1,20 @@
/**
* Database-backed hooks for artists
*/
import { useMemo } from 'react';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
import artists from './entity';
import { and, eq } from 'drizzle-orm';
export function useArtists(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(artists).where(eq(artists.sourceId, sourceId))
: db.select().from(artists)
return useLiveQuery(
db.query.artists.findMany({
where: sourceId ? eq(artists.sourceId, sourceId) : undefined,
})
);
return useMemo(() => ({
data: data ?? [],
error,
}), [data, error]);
}
export function useArtist([sourceId, id]: [sourceId: string, id: string]) {
const { data, error } = useLiveQuery(
db.select()
.from(artists)
.where(and(eq(artists.sourceId, sourceId), eq(artists.id, id)))
.limit(1)
return useLiveQuery(
db.query.artists.findFirst({
where: and(eq(artists.sourceId, sourceId), eq(artists.id, id)),
})
);
return useMemo(() => ({
data: data?.[0],
error,
}), [data, error]);
}
+8 -31
View File
@@ -1,43 +1,20 @@
/**
* Database-backed hooks for downloads data
*/
import { useMemo } from 'react';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
import downloads from './entity';
import { and, eq } from 'drizzle-orm';
import type { Download } from './types';
export function useDownloads(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(downloads).where(eq(downloads.sourceId, sourceId))
: db.select().from(downloads)
return useLiveQuery(
db.query.downloads.findMany({
where: sourceId ? eq(downloads.sourceId, sourceId) : undefined,
})
);
return {
data: data ?? [],
error
};
}
export function useDownload([sourceId, trackId]: [sourceId: string, trackId: string]) {
const { data, error } = useLiveQuery(
db.select()
.from(downloads)
.where(and(eq(downloads.sourceId, sourceId), eq(downloads.id, trackId)))
.limit(1)
return useLiveQuery(
db.query.downloads.findFirst({
where: and(eq(downloads.sourceId, sourceId), eq(downloads.id, trackId)),
})
);
return useMemo(() => ({
data: data?.[0],
error,
}), [data, error]);
}
export function useIsDownloaded([sourceId, trackId]: [sourceId: string, trackId: string]): boolean {
const { data } = useDownload([sourceId, trackId]);
return data?.isComplete === true;
}
+2 -2
View File
@@ -5,7 +5,7 @@ import migrations from './database/migrations/migrations.js';
// Import all schema tables
import sources from './sources/entity';
import appSettings from './settings/entity.js';
import settings from './settings/entity';
import sleepTimer from './sleep-timer/entity';
import artists from './artists/entity';
import albums from './albums/entity';
@@ -22,7 +22,7 @@ import syncCursors from './sync-cursors/entity';
// Combined schema for drizzle
const schema = {
sources,
appSettings,
settings,
sleepTimer,
artists,
albums,
-355
View File
@@ -1,355 +0,0 @@
/**
* Database-backed hooks for music data
* These replace Redux selectors with live database queries
*/
import { useMemo } from 'react';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
import albums from '@/store/albums/entity';
import artists from '@/store/artists/entity';
import tracks from '@/store/tracks/entity';
import playlists from '@/store/playlists/entity';
import playlistTracks from '@/store/playlist-tracks/entity';
import { and, eq, desc, inArray } from 'drizzle-orm';
import { ALPHABET_LETTERS } from '@/CONSTANTS';
import type { Album, AlbumTrack, MusicArtist, Playlist } from './types';
type AlbumSection = { label: string; data: string[][] };
type ArtistSection = { label: string; data: MusicArtist[] };
const createAlbumSection = (label: string): AlbumSection => ({ label, data: [[]] });
const createArtistSection = (label: string): ArtistSection => ({ label, data: [] });
/**
* Get all albums (from all sources)
*/
export function useAlbums(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(albums).where(eq(albums.sourceId, sourceId))
: db.select().from(albums)
);
return useMemo(() => {
const albumsMap: Record<string, Album> = {};
const ids: string[] = [];
let lastRefreshed: Date | undefined;
(data || []).forEach(album => {
const enriched = enrichAlbum(album);
albumsMap[enriched.Id] = enriched;
ids.push(enriched.Id);
// Track the oldest lastRefreshed date
if (enriched.lastRefreshed) {
const refreshDate = new Date(enriched.lastRefreshed);
if (!lastRefreshed || refreshDate < lastRefreshed) {
lastRefreshed = refreshDate;
}
}
});
return { albums: albumsMap, ids, error, isLoading: false, lastRefreshed };
}, [data, error]);
}
/**
* Get recent albums (sorted by date created, from all sources)
*/
export function useRecentAlbums(amount: number = 24, sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(albums).where(eq(albums.sourceId, sourceId)).orderBy(desc(albums.dateCreated)).limit(amount)
: db.select().from(albums).orderBy(desc(albums.dateCreated)).limit(amount)
);
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() {
const { albums: albumsMap, ids } = useAlbums();
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 = ALPHABET_LETTERS.split('').map(createAlbumSection);
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.push([]);
}
});
return sections;
}, [albumsMap, ids]);
}
/**
* Get all artists (from all sources)
*/
export function useArtists(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(artists).where(eq(artists.sourceId, sourceId))
: db.select().from(artists)
);
return useMemo(() => {
const artistsMap: Record<string, MusicArtist> = {};
const ids: string[] = [];
let lastRefreshed: Date | undefined;
(data || []).forEach(artist => {
const enriched = enrichArtist(artist);
artistsMap[enriched.Id] = enriched;
ids.push(enriched.Id);
// Track the oldest lastRefreshed date
if (enriched.lastRefreshed) {
const refreshDate = new Date(enriched.lastRefreshed);
if (!lastRefreshed || refreshDate < lastRefreshed) {
lastRefreshed = refreshDate;
}
}
});
return { artists: artistsMap, ids, error, isLoading: false, lastRefreshed };
}, [data, error]);
}
/**
* Get artists sorted by alphabet with sections
*/
export function useArtistsByAlphabet() {
const { artists: artistsMap } = useArtists();
return useMemo(() => {
const artistsList = Object.values(artistsMap);
const sections = ALPHABET_LETTERS.split('').map(createArtistSection);
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.push(artist);
});
return sections;
}, [artistsMap]);
}
/**
* Get all playlists (from all sources)
*/
export function usePlaylists(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(playlists).where(eq(playlists.sourceId, sourceId))
: db.select().from(playlists)
);
return useMemo(() => {
const playlistsMap: Record<string, Playlist> = {};
const ids: string[] = [];
let lastRefreshed: Date | undefined;
(data || []).forEach(playlist => {
const enriched = enrichPlaylist(playlist);
playlistsMap[enriched.Id] = enriched;
ids.push(enriched.Id);
// Track the oldest lastRefreshed date
if (enriched.lastRefreshed) {
const refreshDate = new Date(enriched.lastRefreshed);
if (!lastRefreshed || refreshDate < lastRefreshed) {
lastRefreshed = refreshDate;
}
}
});
return { playlists: playlistsMap, ids, error, isLoading: false, lastRefreshed };
}, [data, error]);
}
/**
* Get tracks by album
*/
export function useTracksByAlbum([sourceId, albumId]: [sourceId: string, albumId: string]) {
const { data, error } = useLiveQuery(
db.select()
.from(tracks)
.where(and(eq(tracks.sourceId, sourceId), eq(tracks.albumId, albumId)))
);
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 tracks by playlist
*/
export function useTracksByPlaylist([sourceId, playlistId]: [sourceId: string, playlistId: string]) {
const { data: relations, error: relError } = useLiveQuery(
db.select()
.from(playlistTracks)
.where(and(eq(playlistTracks.sourceId, sourceId), eq(playlistTracks.playlistId, playlistId)))
);
const trackIds = useMemo(() => (relations || []).map(r => r.trackId), [relations]);
const { data: tracksData, error: tracksError } = useLiveQuery(
trackIds.length > 0
? db.select()
.from(tracks)
.where(and(eq(tracks.sourceId, sourceId), inArray(tracks.id, trackIds)))
: null
);
return useMemo(() => {
const tracksMap: Record<string, AlbumTrack> = {};
// Create map for quick lookup
(tracksData || []).forEach(track => {
const enriched = enrichTrack(track);
tracksMap[enriched.Id] = enriched;
});
// Sort by position in playlist
const sortedIds = (relations || [])
.sort((a, b) => (a.position || 0) - (b.position || 0))
.map(r => r.trackId);
return { tracks: tracksMap, ids: sortedIds, error: relError || tracksError };
}, [relations, tracksData, relError, tracksError]);
}
/**
* Get all tracks (from all sources)
*/
export function useTracks(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(tracks).where(eq(tracks.sourceId, sourceId))
: db.select().from(tracks)
);
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, isLoading: false };
}, [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,
};
}
+8 -20
View File
@@ -2,35 +2,23 @@
* Database-backed hooks for playlists
*/
import { useMemo } from 'react';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
import playlists from './entity';
import { and, eq } from 'drizzle-orm';
export function usePlaylists(sourceId?: string) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(playlists).where(eq(playlists.sourceId, sourceId))
: db.select().from(playlists)
return useLiveQuery(
db.query.playlists.findMany({
where: sourceId ? eq(playlists.sourceId, sourceId) : undefined,
})
);
return useMemo(() => ({
data: data ?? [],
error,
}), [data, error]);
}
export function usePlaylist([sourceId, id]: [sourceId: string, id: string]) {
const { data, error } = useLiveQuery(
db.select()
.from(playlists)
.where(and(eq(playlists.sourceId, sourceId), eq(playlists.id, id)))
.limit(1)
return useLiveQuery(
db.query.playlists.findFirst({
where: and(eq(playlists.sourceId, sourceId), eq(playlists.id, id)),
})
);
return useMemo(() => ({
data: data?.[0],
error,
}), [data, error]);
}
+7 -11
View File
@@ -2,21 +2,17 @@
* Database-backed hooks for search queries
*/
import { useMemo } from 'react';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
import searchQueries from './entity';
import { eq, desc } from 'drizzle-orm';
import { eq } from 'drizzle-orm';
export function useSearchQueries(sourceId?: string, limit?: number) {
const { data, error } = useLiveQuery(
sourceId
? db.select().from(searchQueries).where(eq(searchQueries.sourceId, sourceId)).orderBy(desc(searchQueries.timestamp)).limit(limit || 100)
: db.select().from(searchQueries).orderBy(desc(searchQueries.timestamp)).limit(limit || 100)
return useLiveQuery(
db.query.searchQueries.findMany({
where: sourceId ? eq(searchQueries.sourceId, sourceId) : undefined,
orderBy: (query, { desc }) => [desc(query.timestamp)],
limit: limit || 100,
})
);
return useMemo(() => ({
data: data ?? [],
error,
}), [data, error]);
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
/**
* App settings - global application settings (single row, id=1)
*/
const settings = sqliteTable('app_settings', {
const settings = sqliteTable('settings', {
id: integer('id').primaryKey().$default(() => 1),
bitrate: integer('bitrate').notNull(),
isOnboardingComplete: integer('is_onboarding_complete', { mode: 'boolean' }).notNull(),
+4 -6
View File
@@ -8,11 +8,9 @@ import { eq } from 'drizzle-orm';
import settings from './entity';
export function useAppSettings() {
const { data, error } = useLiveQuery(
db.select().from(settings)
.where(eq(settings.id, 1))
.limit(1)
return useLiveQuery(
db.query.settings.findFirst({
where: eq(settings.id, 1),
})
);
return { data: data?.[0], error };
}
-59
View File
@@ -1,59 +0,0 @@
/**
* Sleep Timer Database Operations
*
* Replaces Redux store with direct database operations for sleep timer.
*/
import { db } from '../database/client';
import sleepTimer from './entity';
import { eq } from 'drizzle-orm';
import { invalidateTable } from '../live-queries';
const SLEEP_TIMER_ID = 1;
/**
* Get the current sleep timer date
*/
export async function getSleepTimerDate(): Promise<number | null> {
const result = await db
.select()
.from(sleepTimer)
.where(eq(sleepTimer.id, SLEEP_TIMER_ID))
.limit(1);
return result[0]?.date ?? null;
}
/**
* Set the sleep timer date
*/
export async function setSleepTimerDate(date: Date | null): Promise<void> {
const now = Date.now();
const dateValue = date?.getTime() ?? null;
await db
.insert(sleepTimer)
.values({
id: SLEEP_TIMER_ID,
date: dateValue,
createdAt: now,
updatedAt: now,
})
.onConflictDoUpdate({
target: [sleepTimer.id],
set: {
date: dateValue,
updatedAt: now,
},
});
// Invalidate to trigger live query updates
invalidateTable('sleep_timer');
}
/**
* Clear the sleep timer
*/
export async function clearSleepTimer(): Promise<void> {
await setSleepTimerDate(null);
}
+4 -12
View File
@@ -1,20 +1,12 @@
/**
* Database-backed hooks for sleep timer
*/
import { useMemo } from 'react';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
import sleepTimer from './entity';
import { eq } from 'drizzle-orm';
export function useSleepTimer() {
const { data, error } = useLiveQuery(
db.select().from(sleepTimer).where(eq(sleepTimer.id, 1)).limit(1)
return useLiveQuery(
db.query.sleepTimer.findFirst({
where: eq(sleepTimer.id, 1),
})
);
return useMemo(() => ({
data: data?.[0],
error,
}), [data, error]);
}
+6 -11
View File
@@ -1,7 +1,3 @@
/**
* Database-backed hooks for tracks with download joins
*/
import { and, eq } from 'drizzle-orm';
import { useLiveQuery } from '@/store/live-queries';
import { db } from '@/store';
@@ -9,17 +5,16 @@ import tracks from './entity';
export function useTracks(sourceId?: string) {
return useLiveQuery(
sourceId
? db.select().from(tracks).where(eq(tracks.sourceId, sourceId))
: db.select().from(tracks)
db.query.tracks.findMany({
where: sourceId ? eq(tracks.sourceId, sourceId) : undefined,
})
);
}
export function useTrack([sourceId, id]: [sourceId: string, id: string]) {
return useLiveQuery(
db.select()
.from(tracks)
.where(and(eq(tracks.sourceId, sourceId), eq(tracks.id, id)))
.limit(1)
db.query.tracks.findFirst({
where: and(eq(tracks.sourceId, sourceId), eq(tracks.id, id)),
})
);
}