Files
shark/src/lib/library/database.ts
Markury d774aba0d4 feat(dz): add cache clearing and database reset functionality
Add ability to fully clear cached online library by deleting and recreating the database file.
Integrate new Clear Cache option in settings UI, which restarts the app after clearing.
Remove unused artist/album fields from cache and UI. Add process plugin for relaunch.
2025-10-02 13:40:13 -04:00

234 lines
5.7 KiB
TypeScript

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]
);
const artistId = artists[0]?.id ?? result.lastInsertId;
if (artistId == null) {
throw new Error('Failed to get artist ID from upsert operation');
}
return artistId;
}
/**
* 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
};
}