mirror of
https://github.com/leinelissen/jellyfin-audio-player.git
synced 2026-09-02 21:03:11 +03:00
Create database CRUD operations for all modules (settings, search, downloads, music)
Co-authored-by: leinelissen <10154841+leinelissen@users.noreply.github.com>
This commit is contained in:
co-authored by
leinelissen
parent
81df4586fd
commit
d5041e0f79
@@ -0,0 +1,193 @@
|
||||
import { db, sqliteDb } from '@/store/db';
|
||||
import { downloads } from '@/store/db/schema/downloads';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export interface Download {
|
||||
sourceId: string;
|
||||
id: string;
|
||||
hash: string | null;
|
||||
filename: string | null;
|
||||
mimetype: string | null;
|
||||
progress: number | null;
|
||||
isFailed: boolean;
|
||||
isComplete: boolean;
|
||||
metadataJson: string | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface DownloadMetadata {
|
||||
size?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all downloads for a source
|
||||
*/
|
||||
export async function getAllDownloads(sourceId: string): Promise<Download[]> {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(downloads)
|
||||
.where(eq(downloads.sourceId, sourceId));
|
||||
|
||||
return result as Download[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single download by id
|
||||
*/
|
||||
export async function getDownload(id: string): Promise<Download | undefined> {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(downloads)
|
||||
.where(eq(downloads.id, id))
|
||||
.limit(1);
|
||||
|
||||
return result[0] as Download | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a download
|
||||
*/
|
||||
export async function initializeDownload(
|
||||
sourceId: string,
|
||||
id: string,
|
||||
hash?: string,
|
||||
filename?: string,
|
||||
mimetype?: string
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
|
||||
await db.insert(downloads).values({
|
||||
sourceId,
|
||||
id,
|
||||
hash: hash || null,
|
||||
filename: filename || null,
|
||||
mimetype: mimetype || null,
|
||||
progress: 0,
|
||||
isFailed: false,
|
||||
isComplete: false,
|
||||
metadataJson: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).onConflictDoUpdate({
|
||||
target: downloads.id,
|
||||
set: {
|
||||
hash: hash || null,
|
||||
filename: filename || null,
|
||||
mimetype: mimetype || null,
|
||||
progress: 0,
|
||||
isFailed: false,
|
||||
isComplete: false,
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download progress
|
||||
*/
|
||||
export async function updateDownloadProgress(
|
||||
id: string,
|
||||
progress: number,
|
||||
metadata?: DownloadMetadata
|
||||
): Promise<void> {
|
||||
const updates: any = {
|
||||
progress,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
if (metadata) {
|
||||
updates.metadataJson = JSON.stringify(metadata);
|
||||
}
|
||||
|
||||
await db.update(downloads)
|
||||
.set(updates)
|
||||
.where(eq(downloads.id, id));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark download as complete
|
||||
*/
|
||||
export async function completeDownload(
|
||||
id: string,
|
||||
hash?: string,
|
||||
filename?: string
|
||||
): Promise<void> {
|
||||
const updates: any = {
|
||||
isComplete: true,
|
||||
isFailed: false,
|
||||
progress: 100,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
if (hash) updates.hash = hash;
|
||||
if (filename) updates.filename = filename;
|
||||
|
||||
await db.update(downloads)
|
||||
.set(updates)
|
||||
.where(eq(downloads.id, id));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark download as failed
|
||||
*/
|
||||
export async function failDownload(id: string, error?: string): Promise<void> {
|
||||
const metadata = error ? JSON.stringify({ error }) : null;
|
||||
|
||||
await db.update(downloads)
|
||||
.set({
|
||||
isFailed: true,
|
||||
isComplete: false,
|
||||
progress: 0,
|
||||
metadataJson: metadata,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(downloads.id, id));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a download
|
||||
*/
|
||||
export async function removeDownload(id: string): Promise<void> {
|
||||
await db.delete(downloads).where(eq(downloads.id, id));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse download metadata
|
||||
*/
|
||||
export function parseDownloadMetadata(download: Download): DownloadMetadata {
|
||||
if (!download.metadataJson) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(download.metadataJson);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download with parsed metadata
|
||||
*/
|
||||
export interface DownloadWithMetadata extends Download {
|
||||
size?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function enrichDownload(download: Download): DownloadWithMetadata {
|
||||
const metadata = parseDownloadMetadata(download);
|
||||
return {
|
||||
...download,
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
import { db, sqliteDb } 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 { albumArtists } from '@/store/db/schema/album-artists';
|
||||
import { trackArtists } from '@/store/db/schema/track-artists';
|
||||
import { playlistTracks } from '@/store/db/schema/playlist-tracks';
|
||||
import { albumSimilar } from '@/store/db/schema/album-similar';
|
||||
import { eq, and, desc, inArray } from 'drizzle-orm';
|
||||
import type { Album, AlbumTrack, MusicArtist, Playlist } from './types';
|
||||
|
||||
/**
|
||||
* ALBUMS
|
||||
*/
|
||||
|
||||
export async function getAllAlbums(sourceId: string) {
|
||||
const result = await db.select().from(albums).where(eq(albums.sourceId, sourceId));
|
||||
return result.map(album => enrichAlbum(album));
|
||||
}
|
||||
|
||||
export async function getAlbum(id: string) {
|
||||
const result = await db.select().from(albums).where(eq(albums.id, id)).limit(1);
|
||||
if (!result[0]) return undefined;
|
||||
return enrichAlbum(result[0]);
|
||||
}
|
||||
|
||||
export async function getRecentAlbums(sourceId: string, limit: number = 50) {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(albums)
|
||||
.where(eq(albums.sourceId, sourceId))
|
||||
.orderBy(desc(albums.dateCreated))
|
||||
.limit(limit);
|
||||
return result.map(album => enrichAlbum(album));
|
||||
}
|
||||
|
||||
export async function upsertAlbum(sourceId: string, album: Album) {
|
||||
const now = Date.now();
|
||||
const metadata = extractAlbumMetadata(album);
|
||||
|
||||
await db.insert(albums).values({
|
||||
sourceId,
|
||||
id: album.Id,
|
||||
name: album.Name,
|
||||
productionYear: album.ProductionYear || null,
|
||||
isFolder: album.IsFolder,
|
||||
albumArtist: album.AlbumArtist || null,
|
||||
dateCreated: album.DateCreated ? new Date(album.DateCreated).getTime() : now,
|
||||
lastRefreshed: now,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).onConflictDoUpdate({
|
||||
target: albums.id,
|
||||
set: {
|
||||
name: album.Name,
|
||||
productionYear: album.ProductionYear || null,
|
||||
albumArtist: album.AlbumArtist || null,
|
||||
lastRefreshed: now,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
// Update album-artist relations
|
||||
await updateAlbumArtists(sourceId, album.Id, album.AlbumArtists);
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function upsertAlbums(sourceId: string, albumList: Album[]) {
|
||||
for (const album of albumList) {
|
||||
await upsertAlbum(sourceId, album);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSimilarAlbums(albumId: string) {
|
||||
const relations = await db
|
||||
.select()
|
||||
.from(albumSimilar)
|
||||
.where(eq(albumSimilar.albumId, albumId));
|
||||
|
||||
if (relations.length === 0) return [];
|
||||
|
||||
const similarIds = relations.map(r => r.similarAlbumId);
|
||||
const result = await db
|
||||
.select()
|
||||
.from(albums)
|
||||
.where(inArray(albums.id, similarIds));
|
||||
|
||||
return result.map(album => enrichAlbum(album));
|
||||
}
|
||||
|
||||
export async function setSimilarAlbums(sourceId: string, albumId: string, similarAlbumIds: string[]) {
|
||||
// Delete existing relations
|
||||
await db.delete(albumSimilar).where(eq(albumSimilar.albumId, albumId));
|
||||
|
||||
// Insert new relations
|
||||
if (similarAlbumIds.length > 0) {
|
||||
await db.insert(albumSimilar).values(
|
||||
similarAlbumIds.map(similarId => ({
|
||||
sourceId,
|
||||
albumId,
|
||||
similarAlbumId: similarId,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* ARTISTS
|
||||
*/
|
||||
|
||||
export async function getAllArtists(sourceId: string) {
|
||||
const result = await db.select().from(artists).where(eq(artists.sourceId, sourceId));
|
||||
return result.map(artist => enrichArtist(artist));
|
||||
}
|
||||
|
||||
export async function upsertArtist(sourceId: string, artist: MusicArtist) {
|
||||
const now = Date.now();
|
||||
const metadata = extractArtistMetadata(artist);
|
||||
|
||||
await db.insert(artists).values({
|
||||
sourceId,
|
||||
id: artist.Id,
|
||||
name: artist.Name,
|
||||
isFolder: artist.IsFolder,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).onConflictDoUpdate({
|
||||
target: artists.id,
|
||||
set: {
|
||||
name: artist.Name,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function upsertArtists(sourceId: string, artistList: MusicArtist[]) {
|
||||
for (const artist of artistList) {
|
||||
await upsertArtist(sourceId, artist);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TRACKS
|
||||
*/
|
||||
|
||||
export async function getTracksByAlbum(albumId: string) {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(tracks)
|
||||
.where(eq(tracks.albumId, albumId));
|
||||
return result.map(track => enrichTrack(track));
|
||||
}
|
||||
|
||||
export async function getTracksByPlaylist(playlistId: string) {
|
||||
const relations = await db
|
||||
.select()
|
||||
.from(playlistTracks)
|
||||
.where(eq(playlistTracks.playlistId, playlistId));
|
||||
|
||||
if (relations.length === 0) return [];
|
||||
|
||||
const trackIds = relations.map(r => r.trackId);
|
||||
const result = await db
|
||||
.select()
|
||||
.from(tracks)
|
||||
.where(inArray(tracks.id, trackIds));
|
||||
|
||||
// Sort by position in playlist
|
||||
const trackMap = new Map(result.map(t => [t.id, t]));
|
||||
return relations
|
||||
.sort((a, b) => (a.position || 0) - (b.position || 0))
|
||||
.map(r => trackMap.get(r.trackId))
|
||||
.filter(Boolean)
|
||||
.map(track => enrichTrack(track!));
|
||||
}
|
||||
|
||||
export async function getTrack(id: string) {
|
||||
const result = await db.select().from(tracks).where(eq(tracks.id, id)).limit(1);
|
||||
if (!result[0]) return undefined;
|
||||
return enrichTrack(result[0]);
|
||||
}
|
||||
|
||||
export async function upsertTrack(sourceId: string, track: AlbumTrack) {
|
||||
const now = Date.now();
|
||||
const metadata = extractTrackMetadata(track);
|
||||
|
||||
await db.insert(tracks).values({
|
||||
sourceId,
|
||||
id: track.Id,
|
||||
name: track.Name,
|
||||
albumId: track.AlbumId || null,
|
||||
album: track.Album || null,
|
||||
albumArtist: track.AlbumArtist || null,
|
||||
productionYear: track.ProductionYear || null,
|
||||
indexNumber: track.IndexNumber || null,
|
||||
parentIndexNumber: track.ParentIndexNumber || null,
|
||||
hasLyrics: track.HasLyrics || false,
|
||||
runTimeTicks: track.RunTimeTicks || null,
|
||||
lyrics: track.Lyrics ? JSON.stringify(track.Lyrics) : null,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).onConflictDoUpdate({
|
||||
target: tracks.id,
|
||||
set: {
|
||||
name: track.Name,
|
||||
album: track.Album || null,
|
||||
albumArtist: track.AlbumArtist || null,
|
||||
hasLyrics: track.HasLyrics || false,
|
||||
lyrics: track.Lyrics ? JSON.stringify(track.Lyrics) : null,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
// Update track-artist relations
|
||||
await updateTrackArtists(sourceId, track.Id, track.ArtistItems);
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function upsertTracks(sourceId: string, trackList: AlbumTrack[]) {
|
||||
for (const track of trackList) {
|
||||
await upsertTrack(sourceId, track);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTrackCodec(trackId: string, codec: any) {
|
||||
const track = await getTrack(trackId);
|
||||
if (!track) return;
|
||||
|
||||
const metadata = track.metadataJson ? JSON.parse(track.metadataJson) : {};
|
||||
metadata.Codec = codec;
|
||||
|
||||
await db.update(tracks)
|
||||
.set({
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(tracks.id, trackId));
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* PLAYLISTS
|
||||
*/
|
||||
|
||||
export async function getAllPlaylists(sourceId: string) {
|
||||
const result = await db.select().from(playlists).where(eq(playlists.sourceId, sourceId));
|
||||
return result.map(playlist => enrichPlaylist(playlist));
|
||||
}
|
||||
|
||||
export async function getPlaylist(id: string) {
|
||||
const result = await db.select().from(playlists).where(eq(playlists.id, id)).limit(1);
|
||||
if (!result[0]) return undefined;
|
||||
return enrichPlaylist(result[0]);
|
||||
}
|
||||
|
||||
export async function upsertPlaylist(sourceId: string, playlist: Playlist) {
|
||||
const now = Date.now();
|
||||
const metadata = extractPlaylistMetadata(playlist);
|
||||
|
||||
await db.insert(playlists).values({
|
||||
sourceId,
|
||||
id: playlist.Id,
|
||||
name: playlist.Name,
|
||||
canDelete: playlist.CanDelete,
|
||||
childCount: playlist.ChildCount || null,
|
||||
lastRefreshed: now,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}).onConflictDoUpdate({
|
||||
target: playlists.id,
|
||||
set: {
|
||||
name: playlist.Name,
|
||||
canDelete: playlist.CanDelete,
|
||||
childCount: playlist.ChildCount || null,
|
||||
lastRefreshed: now,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
export async function upsertPlaylists(sourceId: string, playlistList: Playlist[]) {
|
||||
for (const playlist of playlistList) {
|
||||
await upsertPlaylist(sourceId, playlist);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setPlaylistTracks(sourceId: string, playlistId: string, trackIds: string[]) {
|
||||
// Delete existing relations
|
||||
await db.delete(playlistTracks).where(
|
||||
and(
|
||||
eq(playlistTracks.sourceId, sourceId),
|
||||
eq(playlistTracks.playlistId, playlistId)
|
||||
)
|
||||
);
|
||||
|
||||
// Insert new relations with positions
|
||||
if (trackIds.length > 0) {
|
||||
await db.insert(playlistTracks).values(
|
||||
trackIds.map((trackId, index) => ({
|
||||
sourceId,
|
||||
playlistId,
|
||||
trackId,
|
||||
position: index,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* HELPER FUNCTIONS
|
||||
*/
|
||||
|
||||
async function updateAlbumArtists(sourceId: string, albumId: string, albumArtistsList: any[]) {
|
||||
// Delete existing relations
|
||||
await db.delete(albumArtists).where(
|
||||
and(
|
||||
eq(albumArtists.sourceId, sourceId),
|
||||
eq(albumArtists.albumId, albumId)
|
||||
)
|
||||
);
|
||||
|
||||
// Insert new relations
|
||||
if (albumArtistsList && albumArtistsList.length > 0) {
|
||||
await db.insert(albumArtists).values(
|
||||
albumArtistsList.map((artist, index) => ({
|
||||
sourceId,
|
||||
albumId,
|
||||
artistId: artist.Id,
|
||||
orderIndex: index,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTrackArtists(sourceId: string, trackId: string, artistItems: any[]) {
|
||||
// Delete existing relations
|
||||
await db.delete(trackArtists).where(
|
||||
and(
|
||||
eq(trackArtists.sourceId, sourceId),
|
||||
eq(trackArtists.trackId, trackId)
|
||||
)
|
||||
);
|
||||
|
||||
// Insert new relations
|
||||
if (artistItems && artistItems.length > 0) {
|
||||
await db.insert(trackArtists).values(
|
||||
artistItems.map((artist, index) => ({
|
||||
sourceId,
|
||||
trackId,
|
||||
artistId: artist.Id,
|
||||
orderIndex: index,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function extractAlbumMetadata(album: Album) {
|
||||
return {
|
||||
ServerId: album.ServerId,
|
||||
SortName: album.SortName,
|
||||
RunTimeTicks: album.RunTimeTicks,
|
||||
Type: album.Type,
|
||||
UserData: album.UserData,
|
||||
PrimaryImageAspectRatio: album.PrimaryImageAspectRatio,
|
||||
Artists: album.Artists,
|
||||
ArtistItems: album.ArtistItems,
|
||||
AlbumArtists: album.AlbumArtists,
|
||||
ImageTags: album.ImageTags,
|
||||
BackdropImageTags: album.BackdropImageTags,
|
||||
LocationType: album.LocationType,
|
||||
Overview: album.Overview,
|
||||
PrimaryImageItemId: album.PrimaryImageItemId,
|
||||
};
|
||||
}
|
||||
|
||||
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 extractArtistMetadata(artist: MusicArtist) {
|
||||
return {
|
||||
ServerId: artist.ServerId,
|
||||
ChannelId: artist.ChannelId,
|
||||
RunTimeTicks: artist.RunTimeTicks,
|
||||
Type: artist.Type,
|
||||
UserData: artist.UserData,
|
||||
ImageTags: artist.ImageTags,
|
||||
BackdropImageTags: artist.BackdropImageTags,
|
||||
ImageBlurHashes: artist.ImageBlurHashes,
|
||||
LocationType: artist.LocationType,
|
||||
Overview: artist.Overview,
|
||||
};
|
||||
}
|
||||
|
||||
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 extractTrackMetadata(track: AlbumTrack) {
|
||||
return {
|
||||
ServerId: track.ServerId,
|
||||
Type: track.Type,
|
||||
UserData: track.UserData,
|
||||
Artists: track.Artists,
|
||||
ArtistItems: track.ArtistItems,
|
||||
AlbumPrimaryImageTag: track.AlbumPrimaryImageTag,
|
||||
AlbumArtists: track.AlbumArtists,
|
||||
ImageTags: track.ImageTags,
|
||||
BackdropImageTags: track.BackdropImageTags,
|
||||
LocationType: track.LocationType,
|
||||
MediaType: track.MediaType,
|
||||
MediaStreams: track.MediaStreams,
|
||||
Codec: track.Codec,
|
||||
};
|
||||
}
|
||||
|
||||
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 extractPlaylistMetadata(playlist: Playlist) {
|
||||
return {
|
||||
ServerId: playlist.ServerId,
|
||||
SortName: playlist.SortName,
|
||||
ChannelId: playlist.ChannelId,
|
||||
RunTimeTicks: playlist.RunTimeTicks,
|
||||
Type: playlist.Type,
|
||||
UserData: playlist.UserData,
|
||||
PrimaryImageAspectRatio: playlist.PrimaryImageAspectRatio,
|
||||
ImageTags: playlist.ImageTags,
|
||||
BackdropImageTags: playlist.BackdropImageTags,
|
||||
LocationType: playlist.LocationType,
|
||||
MediaType: playlist.MediaType,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { db, sqliteDb } from '@/store/db';
|
||||
import { searchQueries } from '@/store/db/schema/search-queries';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
|
||||
type SearchType = 'Audio' | 'MusicAlbum' | 'MusicArtist' | 'Playlist';
|
||||
|
||||
export interface SearchQuery {
|
||||
sourceId: string;
|
||||
id: string;
|
||||
query: string;
|
||||
timestamp: number;
|
||||
localPlaybackOnly: boolean;
|
||||
metadataJson: string | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface SearchQueryDisplay {
|
||||
query: string;
|
||||
filters: SearchType[];
|
||||
localPlaybackOnly: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search history for a source (ordered by timestamp desc, limit 10)
|
||||
*/
|
||||
export async function getSearchHistory(sourceId: string): Promise<SearchQuery[]> {
|
||||
const result = await db
|
||||
.select()
|
||||
.from(searchQueries)
|
||||
.where(eq(searchQueries.sourceId, sourceId))
|
||||
.orderBy(desc(searchQueries.timestamp))
|
||||
.limit(10);
|
||||
|
||||
return result as SearchQuery[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a search query to history
|
||||
*/
|
||||
export async function addSearchQuery(
|
||||
sourceId: string,
|
||||
query: string,
|
||||
filters: SearchType[],
|
||||
localPlaybackOnly: boolean
|
||||
): Promise<void> {
|
||||
const now = Date.now();
|
||||
const id = `${sourceId}-${query}-${filters.sort().join(',')}-${localPlaybackOnly}`;
|
||||
const metadata = { filters };
|
||||
|
||||
// Delete existing query with same parameters (to move it to top)
|
||||
await db.delete(searchQueries).where(eq(searchQueries.id, id));
|
||||
|
||||
// Insert new query
|
||||
await db.insert(searchQueries).values({
|
||||
sourceId,
|
||||
id,
|
||||
query,
|
||||
timestamp: now,
|
||||
localPlaybackOnly,
|
||||
metadataJson: JSON.stringify(metadata),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
// Keep only last 10 queries for this source
|
||||
const allQueries = await getSearchHistory(sourceId);
|
||||
if (allQueries.length > 10) {
|
||||
const idsToDelete = allQueries.slice(10).map(q => q.id);
|
||||
for (const id of idsToDelete) {
|
||||
await db.delete(searchQueries).where(eq(searchQueries.id, id));
|
||||
}
|
||||
}
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all search history for a source
|
||||
*/
|
||||
export async function clearSearchHistory(sourceId: string): Promise<void> {
|
||||
await db.delete(searchQueries).where(eq(searchQueries.sourceId, sourceId));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse search queries to display format
|
||||
*/
|
||||
export function parseSearchQueries(queries: SearchQuery[]): SearchQueryDisplay[] {
|
||||
return queries.map(q => {
|
||||
const metadata = q.metadataJson ? JSON.parse(q.metadataJson) : { filters: [] };
|
||||
return {
|
||||
query: q.query,
|
||||
filters: metadata.filters || [],
|
||||
localPlaybackOnly: q.localPlaybackOnly,
|
||||
timestamp: q.timestamp,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { db, sqliteDb } from '@/store/db';
|
||||
import { appSettings } from '@/store/db/schema/app-settings';
|
||||
import { sources } from '@/store/db/schema/sources';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ColorScheme } from './types';
|
||||
|
||||
export interface AppSettings {
|
||||
id: number;
|
||||
bitrate: number;
|
||||
isOnboardingComplete: boolean;
|
||||
hasReceivedErrorReportingAlert: boolean;
|
||||
enablePlaybackReporting: boolean;
|
||||
colorScheme: ColorScheme;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface SourceCredentials {
|
||||
id: string;
|
||||
uri: string;
|
||||
userId: string | null;
|
||||
accessToken: string | null;
|
||||
deviceId: string | null;
|
||||
type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app settings (single row, id=1)
|
||||
*/
|
||||
export async function getAppSettings(): Promise<AppSettings | undefined> {
|
||||
const result = await db.select().from(appSettings).where(eq(appSettings.id, 1)).limit(1);
|
||||
return result[0] as AppSettings | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize app settings with defaults if not exists
|
||||
*/
|
||||
export async function initializeAppSettings(): Promise<AppSettings> {
|
||||
const existing = await getAppSettings();
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const defaults: typeof appSettings.$inferInsert = {
|
||||
id: 1,
|
||||
bitrate: 140000000,
|
||||
isOnboardingComplete: false,
|
||||
hasReceivedErrorReportingAlert: false,
|
||||
enablePlaybackReporting: true,
|
||||
colorScheme: ColorScheme.System,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await db.insert(appSettings).values(defaults);
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
return defaults as AppSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update app settings
|
||||
*/
|
||||
export async function updateAppSettings(updates: Partial<Omit<AppSettings, 'id' | 'createdAt' | 'updatedAt'>>): Promise<void> {
|
||||
await db.update(appSettings)
|
||||
.set({ ...updates, updatedAt: Date.now() })
|
||||
.where(eq(appSettings.id, 1));
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bitrate
|
||||
*/
|
||||
export async function setBitrate(bitrate: number): Promise<void> {
|
||||
await updateAppSettings({ bitrate });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set onboarding status
|
||||
*/
|
||||
export async function setOnboardingStatus(isOnboardingComplete: boolean): Promise<void> {
|
||||
await updateAppSettings({ isOnboardingComplete });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set error reporting alert received
|
||||
*/
|
||||
export async function setReceivedErrorReportingAlert(): Promise<void> {
|
||||
await updateAppSettings({ hasReceivedErrorReportingAlert: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set enable playback reporting
|
||||
*/
|
||||
export async function setEnablePlaybackReporting(enablePlaybackReporting: boolean): Promise<void> {
|
||||
await updateAppSettings({ enablePlaybackReporting });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color scheme
|
||||
*/
|
||||
export async function setColorScheme(colorScheme: ColorScheme): Promise<void> {
|
||||
await updateAppSettings({ colorScheme });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active source (credentials)
|
||||
*/
|
||||
export async function getActiveSource(): Promise<SourceCredentials | undefined> {
|
||||
const result = await db.select().from(sources).limit(1);
|
||||
return result[0] as SourceCredentials | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Jellyfin/Emby credentials
|
||||
*/
|
||||
export async function setCredentials(credentials: {
|
||||
uri: string;
|
||||
user_id: string;
|
||||
access_token: string;
|
||||
device_id: string;
|
||||
type: 'jellyfin' | 'emby';
|
||||
}): Promise<void> {
|
||||
const now = Date.now();
|
||||
const sourceType = credentials.type === 'jellyfin' ? 'jellyfin.v1' : 'emby.v1';
|
||||
|
||||
// Use device_id as the source id for consistency
|
||||
const sourceId = credentials.device_id;
|
||||
|
||||
await db.insert(sources)
|
||||
.values({
|
||||
id: sourceId,
|
||||
uri: credentials.uri,
|
||||
userId: credentials.user_id,
|
||||
accessToken: credentials.access_token,
|
||||
deviceId: credentials.device_id,
|
||||
type: sourceType,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: sources.id,
|
||||
set: {
|
||||
uri: credentials.uri,
|
||||
userId: credentials.user_id,
|
||||
accessToken: credentials.access_token,
|
||||
deviceId: credentials.device_id,
|
||||
type: sourceType,
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
|
||||
sqliteDb.flushPendingReactiveQueries();
|
||||
}
|
||||
Reference in New Issue
Block a user