mirror of
https://github.com/leinelissen/jellyfin-audio-player.git
synced 2026-09-02 21:03:11 +03:00
fix: introduce entityid
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import albumArtists from './entity';
|
||||
import type { EntityId } from '@/store/types';
|
||||
import type { Artist } from '../sources/types';
|
||||
|
||||
/**
|
||||
@@ -7,8 +8,7 @@ import type { Artist } from '../sources/types';
|
||||
* Preserves order from the source by storing orderIndex.
|
||||
*/
|
||||
export async function upsertAlbumArtists(
|
||||
sourceId: string,
|
||||
albumId: string,
|
||||
[sourceId, albumId]: EntityId,
|
||||
artistItems: Pick<Artist, 'id'>[],
|
||||
): Promise<void> {
|
||||
if (artistItems.length === 0) return;
|
||||
@@ -26,4 +26,4 @@ export async function upsertAlbumArtists(
|
||||
}
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import albumSimilar from './entity';
|
||||
import type { EntityId } from '@/store/types';
|
||||
|
||||
export async function upsertAlbumSimilar(
|
||||
sourceId: string,
|
||||
albumId: string,
|
||||
[sourceId, albumId]: EntityId,
|
||||
similarAlbumIds: string[],
|
||||
): Promise<void> {
|
||||
if (similarAlbumIds.length === 0) return;
|
||||
@@ -17,4 +17,4 @@ export async function upsertAlbumSimilar(
|
||||
}
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import albums from './entity';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import type { EntityId } from '@/store/types';
|
||||
import type { InsertAlbum } from './types';
|
||||
import { getAllSourceDrivers } from '../sources/actions';
|
||||
|
||||
@@ -46,8 +47,8 @@ export async function upsertAlbums(albumList: UpsertAlbum[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAlbum(id: string): Promise<void> {
|
||||
await db.delete(albums).where(eq(albums.id, id));
|
||||
export async function deleteAlbum([sourceId, id]: EntityId): Promise<void> {
|
||||
await db.delete(albums).where(and(eq(albums.sourceId, sourceId), eq(albums.id, id)));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
@@ -58,5 +59,5 @@ export async function deleteAlbumsBySource(sourceId: string): Promise<void> {
|
||||
|
||||
export async function refreshAlbums(): Promise<void> {
|
||||
// TODO: implement per-driver refresh logic
|
||||
await getAllSourceDrivers();
|
||||
}
|
||||
(await getAllSourceDrivers()).forEach((driver) => driver.getAlbums());
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import artists from './entity';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import type { InsertArtist } from './types';
|
||||
import type { EntityId } from '@/store/types';
|
||||
|
||||
/**
|
||||
* createdAt and updatedAt are optional — they reflect server-reported dates and
|
||||
@@ -43,8 +44,8 @@ export async function upsertArtists(artistList: UpsertArtist[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteArtist(id: string): Promise<void> {
|
||||
await db.delete(artists).where(eq(artists.id, id));
|
||||
export async function deleteArtist([sourceId, id]: EntityId): Promise<void> {
|
||||
await db.delete(artists).where(and(eq(artists.sourceId, sourceId), eq(artists.id, id)));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import downloads from './entity';
|
||||
import type { Download } from './types';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { EntityId } from '@/store/types';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
|
||||
export async function getAllDownloads(): Promise<Download[]> {
|
||||
const result = await db.select().from(downloads);
|
||||
return result as Download[];
|
||||
}
|
||||
|
||||
export async function getDownload(id: string): Promise<Download | undefined> {
|
||||
export async function getDownload([sourceId, id]: EntityId): Promise<Download | undefined> {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(downloads)
|
||||
.where(eq(downloads.id, id))
|
||||
.where(and(eq(downloads.sourceId, sourceId), eq(downloads.id, id)))
|
||||
.limit(1);
|
||||
|
||||
|
||||
return result[0] as Download | undefined;
|
||||
}
|
||||
|
||||
export async function initializeDownload(
|
||||
sourceId: string,
|
||||
id: string,
|
||||
[sourceId, id]: EntityId,
|
||||
hash?: string,
|
||||
filename?: string,
|
||||
mimetype?: string
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
|
||||
|
||||
await db.insert(downloads).values({
|
||||
sourceId,
|
||||
id,
|
||||
@@ -56,7 +56,7 @@ export async function initializeDownload(
|
||||
}
|
||||
|
||||
export async function updateDownloadProgress(
|
||||
id: string,
|
||||
[sourceId, id]: EntityId,
|
||||
progress: number
|
||||
): Promise<void> {
|
||||
await db.update(downloads)
|
||||
@@ -64,16 +64,16 @@ export async function updateDownloadProgress(
|
||||
progress,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(downloads.id, id));
|
||||
.where(and(eq(downloads.sourceId, sourceId), eq(downloads.id, id)));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function completeDownload(
|
||||
id: string,
|
||||
[sourceId, id]: EntityId,
|
||||
filename?: string
|
||||
): Promise<void> {
|
||||
const updates: any = {
|
||||
const updates: Partial<typeof downloads.$inferInsert> = {
|
||||
isComplete: true,
|
||||
isFailed: false,
|
||||
progress: 1,
|
||||
@@ -86,12 +86,12 @@ export async function completeDownload(
|
||||
|
||||
await db.update(downloads)
|
||||
.set(updates)
|
||||
.where(eq(downloads.id, id));
|
||||
.where(and(eq(downloads.sourceId, sourceId), eq(downloads.id, id)));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function failDownload(id: string): Promise<void> {
|
||||
export async function failDownload([sourceId, id]: EntityId): Promise<void> {
|
||||
await db.update(downloads)
|
||||
.set({
|
||||
isFailed: true,
|
||||
@@ -99,12 +99,13 @@ export async function failDownload(id: string): Promise<void> {
|
||||
progress: 0,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(downloads.id, id));
|
||||
.where(and(eq(downloads.sourceId, sourceId), eq(downloads.id, id)));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function removeDownload(id: string): Promise<void> {
|
||||
await db.delete(downloads).where(eq(downloads.id, id));
|
||||
export async function removeDownload([sourceId, id]: EntityId): Promise<void> {
|
||||
await db.delete(downloads)
|
||||
.where(and(eq(downloads.sourceId, sourceId), eq(downloads.id, id)));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ const downloads = sqliteTable('downloads', {
|
||||
isFailed: integer('is_failed', { mode: 'boolean' }).notNull(),
|
||||
isComplete: integer('is_complete', { mode: 'boolean' }).notNull(),
|
||||
metadata: text('metadata'), // JSON-encoded additional fields
|
||||
createdAt: integer('created_at').notNull(),
|
||||
updatedAt: integer('updated_at').notNull(),
|
||||
createdAt: integer('created_at').notNull().$defaultFn(() => Date.now()),
|
||||
updatedAt: integer('updated_at').notNull().$defaultFn(() => Date.now()).$onUpdateFn(() => Date.now()),
|
||||
});
|
||||
|
||||
export default downloads;
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { DocumentDirectoryPath, downloadFile, exists, unlink } from 'react-native-fs';
|
||||
import { db } from '@/store';
|
||||
import sources from '@/store/sources/entity';
|
||||
import downloads from '@/store/downloads/entity';
|
||||
import { JellyfinDriver } from '@/store/sources/drivers/jellyfin/driver';
|
||||
import { EmbyDriver } from '@/store/sources/drivers/emby/driver';
|
||||
import type { Source, SourceDriver, SourceType } from '@/store/sources/types';
|
||||
import { initializeDownload, updateDownloadProgress, completeDownload, failDownload, removeDownload } from './actions';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
async function getDriver(): Promise<{ driver: SourceDriver; source: Source } | null> {
|
||||
const result = await db.select().from(sources).limit(1);
|
||||
const row = result[0];
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source: Source = {
|
||||
id: row.id,
|
||||
uri: row.uri,
|
||||
userId: row.userId || undefined,
|
||||
accessToken: row.accessToken || undefined,
|
||||
deviceId: row.deviceId || undefined,
|
||||
type: row.type as SourceType,
|
||||
};
|
||||
|
||||
if (source.type.startsWith('jellyfin')) {
|
||||
return { driver: new JellyfinDriver(source), source };
|
||||
}
|
||||
|
||||
if (source.type.startsWith('emby')) {
|
||||
return { driver: new EmbyDriver(source), source };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function updateDownloadMetadata(id: string, updates: Record<string, unknown>): Promise<void> {
|
||||
const existing = await db.select().from(downloads).where(eq(downloads.id, id)).limit(1);
|
||||
const current = existing[0];
|
||||
const currentMetadata = current?.metadata ? JSON.parse(current.metadata) : {};
|
||||
|
||||
await db.update(downloads)
|
||||
.set({
|
||||
metadata: JSON.stringify({ ...currentMetadata, ...updates }),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(downloads.id, id));
|
||||
}
|
||||
|
||||
export async function queueTrackForDownload(trackId: string): Promise<void> {
|
||||
const driverResult = await getDriver();
|
||||
if (!driverResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
await initializeDownload(driverResult.source.id, trackId);
|
||||
}
|
||||
|
||||
export async function downloadTrack(trackId: string): Promise<void> {
|
||||
const driverResult = await getDriver();
|
||||
if (!driverResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { driver } = driverResult;
|
||||
|
||||
try {
|
||||
const info = await driver.getDownloadInfo(trackId);
|
||||
const destination = `${DocumentDirectoryPath}/${info.filename}`;
|
||||
|
||||
await updateDownloadMetadata(trackId, { size: null, error: null });
|
||||
|
||||
const job = downloadFile({
|
||||
fromUrl: info.url,
|
||||
toFile: destination,
|
||||
background: true,
|
||||
progress: (event) => {
|
||||
if (!event.contentLength) {
|
||||
return;
|
||||
}
|
||||
const progress = event.bytesWritten / event.contentLength;
|
||||
updateDownloadProgress(trackId, progress);
|
||||
updateDownloadMetadata(trackId, { size: event.contentLength });
|
||||
},
|
||||
});
|
||||
|
||||
await job.promise;
|
||||
await completeDownload(trackId, destination);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
await updateDownloadMetadata(trackId, { error: message });
|
||||
await failDownload(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeDownloadedTrack(trackId: string): Promise<void> {
|
||||
const downloadRow = await db.select().from(downloads).where(eq(downloads.id, trackId)).limit(1);
|
||||
const entry = downloadRow[0];
|
||||
|
||||
if (entry?.filename) {
|
||||
const filePath = entry.filename.startsWith('/')
|
||||
? entry.filename
|
||||
: `${DocumentDirectoryPath}/${entry.filename}`;
|
||||
|
||||
if (await exists(filePath)) {
|
||||
await unlink(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
await removeDownload(trackId);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import playlistTracks from './entity';
|
||||
import type { EntityId } from '@/store/types';
|
||||
|
||||
/**
|
||||
* Replaces all tracks for a playlist with the provided ordered list of track IDs.
|
||||
* Deletes existing entries first to handle removals and reordering.
|
||||
*/
|
||||
export async function setPlaylistTracks(
|
||||
sourceId: string,
|
||||
playlistId: string,
|
||||
[sourceId, playlistId]: EntityId,
|
||||
trackIds: string[],
|
||||
): Promise<void> {
|
||||
await db.delete(playlistTracks).where(
|
||||
@@ -30,4 +30,4 @@ export async function setPlaylistTracks(
|
||||
}
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import playlists from './entity';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import type { InsertPlaylist } from './types';
|
||||
import type { EntityId } from '@/store/types';
|
||||
|
||||
/**
|
||||
* createdAt and updatedAt are optional — they reflect server-provided timestamps
|
||||
@@ -43,8 +44,8 @@ export async function upsertPlaylists(playlistList: UpsertPlaylist[]): Promise<v
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePlaylist(id: string): Promise<void> {
|
||||
await db.delete(playlists).where(eq(playlists.id, id));
|
||||
export async function deletePlaylist([sourceId, id]: EntityId): Promise<void> {
|
||||
await db.delete(playlists).where(and(eq(playlists.sourceId, sourceId), eq(playlists.id, id)));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import searchQueries from './entity';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import type { InsertSearchQuery } from './types';
|
||||
import type { EntityId } from '@/store/types';
|
||||
|
||||
export async function upsertSearchQuery(query: InsertSearchQuery): Promise<void> {
|
||||
const now = Date.now();
|
||||
@@ -25,8 +26,8 @@ export async function upsertSearchQuery(query: InsertSearchQuery): Promise<void>
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function deleteSearchQuery(id: string): Promise<void> {
|
||||
await db.delete(searchQueries).where(eq(searchQueries.id, id));
|
||||
export async function deleteSearchQuery([sourceId, id]: EntityId): Promise<void> {
|
||||
await db.delete(searchQueries).where(and(eq(searchQueries.sourceId, sourceId), eq(searchQueries.id, id)));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import trackArtists from './entity';
|
||||
import type { EntityId } from '@/store/types';
|
||||
|
||||
export async function upsertTrackArtists(
|
||||
sourceId: string,
|
||||
trackId: string,
|
||||
[sourceId, trackId]: EntityId,
|
||||
artistItems: { id: string }[],
|
||||
): Promise<void> {
|
||||
if (artistItems.length === 0) return;
|
||||
@@ -21,4 +21,4 @@ export async function upsertTrackArtists(
|
||||
}
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { db, sqliteDb } from '@/store';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import tracks from './entity';
|
||||
import type { EntityId } from '@/store/types';
|
||||
|
||||
/**
|
||||
* Update lyrics content for a track.
|
||||
@@ -9,12 +10,12 @@ import tracks from './entity';
|
||||
* last-modified date, not local operations. lastSyncedAt is bumped automatically
|
||||
* by the schema $onUpdateFn.
|
||||
*/
|
||||
export async function updateTrackLyrics(trackId: string, lyrics: string | null): Promise<void> {
|
||||
export async function updateTrackLyrics([sourceId, trackId]: EntityId, lyrics: string | null): Promise<void> {
|
||||
await db.update(tracks)
|
||||
.set({
|
||||
lyrics,
|
||||
})
|
||||
.where(eq(tracks.id, trackId));
|
||||
.where(and(eq(tracks.sourceId, sourceId), eq(tracks.id, trackId)));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* A tuple that uniquely identifies any entity stored in the local database.
|
||||
*
|
||||
* Because every entity is scoped to a source (Jellyfin / Emby server), both
|
||||
* the source's own identifier and the entity's identifier are required together
|
||||
* to unambiguously address a single row.
|
||||
*
|
||||
* Usage:
|
||||
* const id: EntityId = [sourceId, itemId];
|
||||
* const [sourceId, itemId] = id;
|
||||
*/
|
||||
export type EntityId = [sourceId: string, itemId: string];
|
||||
Reference in New Issue
Block a user