mirror of
https://github.com/markuryy/shark.git
synced 2025-12-15 04:41:01 +00:00
feat(library): add sqlite-backed library sync and stats
BREAKING CHANGE: Library data is now stored in a database and will require an initial sync. Existing in-memory library data is no longer used.
This commit is contained in:
229
src/lib/library/database.ts
Normal file
229
src/lib/library/database.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import Database from '@tauri-apps/plugin-sql';
|
||||
|
||||
export interface DbArtist {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
album_count: number;
|
||||
track_count: number;
|
||||
primary_cover_path: string | null;
|
||||
last_scanned: number | null;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
export interface DbAlbum {
|
||||
id: number;
|
||||
artist_id: number;
|
||||
artist_name: string;
|
||||
title: string;
|
||||
path: string;
|
||||
cover_path: string | null;
|
||||
track_count: number;
|
||||
year: number | null;
|
||||
last_scanned: number | null;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
let db: Database | null = null;
|
||||
|
||||
/**
|
||||
* Initialize database connection
|
||||
*/
|
||||
export async function initDatabase(): Promise<Database> {
|
||||
if (!db) {
|
||||
db = await Database.load('sqlite:library.db');
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all artists with their albums
|
||||
*/
|
||||
export async function getArtistsWithAlbums(): Promise<DbArtist[]> {
|
||||
const database = await initDatabase();
|
||||
const artists = await database.select<DbArtist[]>(
|
||||
'SELECT * FROM artists ORDER BY name COLLATE NOCASE'
|
||||
);
|
||||
return artists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all albums for a specific artist
|
||||
*/
|
||||
export async function getAlbumsByArtist(artistId: number): Promise<DbAlbum[]> {
|
||||
const database = await initDatabase();
|
||||
const albums = await database.select<DbAlbum[]>(
|
||||
'SELECT * FROM albums WHERE artist_id = $1 ORDER BY title COLLATE NOCASE',
|
||||
[artistId]
|
||||
);
|
||||
return albums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all albums across all artists
|
||||
*/
|
||||
export async function getAllAlbums(): Promise<DbAlbum[]> {
|
||||
const database = await initDatabase();
|
||||
const albums = await database.select<DbAlbum[]>(
|
||||
'SELECT * FROM albums ORDER BY artist_name COLLATE NOCASE, title COLLATE NOCASE'
|
||||
);
|
||||
return albums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific album by path
|
||||
*/
|
||||
export async function getAlbumByPath(path: string): Promise<DbAlbum | null> {
|
||||
const database = await initDatabase();
|
||||
const albums = await database.select<DbAlbum[]>(
|
||||
'SELECT * FROM albums WHERE path = $1',
|
||||
[path]
|
||||
);
|
||||
return albums[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert an artist (insert or update)
|
||||
*/
|
||||
export async function upsertArtist(artist: {
|
||||
name: string;
|
||||
path: string;
|
||||
album_count: number;
|
||||
track_count: number;
|
||||
primary_cover_path?: string | null;
|
||||
}): Promise<number> {
|
||||
const database = await initDatabase();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const result = await database.execute(
|
||||
`INSERT INTO artists (name, path, album_count, track_count, primary_cover_path, last_scanned)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT(path) DO UPDATE SET
|
||||
name = $1,
|
||||
album_count = $3,
|
||||
track_count = $4,
|
||||
primary_cover_path = $5,
|
||||
last_scanned = $6`,
|
||||
[
|
||||
artist.name,
|
||||
artist.path,
|
||||
artist.album_count,
|
||||
artist.track_count,
|
||||
artist.primary_cover_path || null,
|
||||
now
|
||||
]
|
||||
);
|
||||
|
||||
// Get the artist ID
|
||||
const artists = await database.select<DbArtist[]>(
|
||||
'SELECT id FROM artists WHERE path = $1',
|
||||
[artist.path]
|
||||
);
|
||||
|
||||
return artists[0]?.id || result.lastInsertId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert an album (insert or update)
|
||||
*/
|
||||
export async function upsertAlbum(album: {
|
||||
artist_id: number;
|
||||
artist_name: string;
|
||||
title: string;
|
||||
path: string;
|
||||
cover_path?: string | null;
|
||||
track_count: number;
|
||||
year?: number | null;
|
||||
}): Promise<void> {
|
||||
const database = await initDatabase();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
await database.execute(
|
||||
`INSERT INTO albums (artist_id, artist_name, title, path, cover_path, track_count, year, last_scanned)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT(path) DO UPDATE SET
|
||||
artist_id = $1,
|
||||
artist_name = $2,
|
||||
title = $3,
|
||||
cover_path = $5,
|
||||
track_count = $6,
|
||||
year = $7,
|
||||
last_scanned = $8`,
|
||||
[
|
||||
album.artist_id,
|
||||
album.artist_name,
|
||||
album.title,
|
||||
album.path,
|
||||
album.cover_path || null,
|
||||
album.track_count,
|
||||
album.year || null,
|
||||
now
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete albums that no longer exist on filesystem
|
||||
*/
|
||||
export async function deleteAlbumsNotInPaths(paths: string[]): Promise<void> {
|
||||
if (paths.length === 0) {
|
||||
// Clear all albums
|
||||
const database = await initDatabase();
|
||||
await database.execute('DELETE FROM albums');
|
||||
return;
|
||||
}
|
||||
|
||||
const database = await initDatabase();
|
||||
const placeholders = paths.map((_, i) => `$${i + 1}`).join(',');
|
||||
await database.execute(
|
||||
`DELETE FROM albums WHERE path NOT IN (${placeholders})`,
|
||||
paths
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete artists that have no albums
|
||||
*/
|
||||
export async function deleteOrphanedArtists(): Promise<void> {
|
||||
const database = await initDatabase();
|
||||
await database.execute(
|
||||
'DELETE FROM artists WHERE id NOT IN (SELECT DISTINCT artist_id FROM albums)'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all library data
|
||||
*/
|
||||
export async function clearLibrary(): Promise<void> {
|
||||
const database = await initDatabase();
|
||||
await database.execute('DELETE FROM albums');
|
||||
await database.execute('DELETE FROM artists');
|
||||
await database.execute('VACUUM'); // Reclaim disk space
|
||||
}
|
||||
|
||||
/**
|
||||
* Get library statistics
|
||||
*/
|
||||
export async function getLibraryStats(): Promise<{
|
||||
artistCount: number;
|
||||
albumCount: number;
|
||||
trackCount: number;
|
||||
}> {
|
||||
const database = await initDatabase();
|
||||
|
||||
const artistResult = await database.select<{ count: number }[]>(
|
||||
'SELECT COUNT(*) as count FROM artists'
|
||||
);
|
||||
const albumResult = await database.select<{ count: number }[]>(
|
||||
'SELECT COUNT(*) as count FROM albums'
|
||||
);
|
||||
const trackResult = await database.select<{ total: number }[]>(
|
||||
'SELECT SUM(track_count) as total FROM albums'
|
||||
);
|
||||
|
||||
return {
|
||||
artistCount: artistResult[0]?.count || 0,
|
||||
albumCount: albumResult[0]?.count || 0,
|
||||
trackCount: trackResult[0]?.total || 0
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user