mirror of
https://github.com/markuryy/shark.git
synced 2025-12-12 11:41:02 +00:00
326 lines
7.9 KiB
TypeScript
326 lines
7.9 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;
|
|
}
|
|
|
|
export interface DbTrack {
|
|
id: number;
|
|
path: string;
|
|
title: string;
|
|
artist: string;
|
|
album: string;
|
|
duration: number;
|
|
format: string;
|
|
has_lyrics: number;
|
|
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
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all tracks without lyrics (has_lyrics = 0)
|
|
*/
|
|
export async function getTracksWithoutLyrics(): Promise<DbTrack[]> {
|
|
const database = await initDatabase();
|
|
const tracks = await database.select<DbTrack[]>(
|
|
'SELECT * FROM tracks WHERE has_lyrics = 0 ORDER BY artist COLLATE NOCASE, album COLLATE NOCASE, title COLLATE NOCASE'
|
|
);
|
|
return tracks;
|
|
}
|
|
|
|
/**
|
|
* Upsert a track (insert or update)
|
|
*/
|
|
export async function upsertTrack(track: {
|
|
path: string;
|
|
title: string;
|
|
artist: string;
|
|
album: string;
|
|
duration: number;
|
|
format: string;
|
|
has_lyrics: boolean;
|
|
}): Promise<void> {
|
|
const database = await initDatabase();
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
await database.execute(
|
|
`INSERT INTO tracks (path, title, artist, album, duration, format, has_lyrics, last_scanned)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
ON CONFLICT(path) DO UPDATE SET
|
|
title = $2,
|
|
artist = $3,
|
|
album = $4,
|
|
duration = $5,
|
|
format = $6,
|
|
has_lyrics = $7,
|
|
last_scanned = $8`,
|
|
[
|
|
track.path,
|
|
track.title,
|
|
track.artist,
|
|
track.album,
|
|
track.duration,
|
|
track.format,
|
|
track.has_lyrics ? 1 : 0,
|
|
now
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the last scan timestamp for lyrics
|
|
*/
|
|
export async function getLyricsScanTimestamp(): Promise<number | null> {
|
|
const database = await initDatabase();
|
|
const result = await database.select<{ last_scanned: number | null }[]>(
|
|
'SELECT MAX(last_scanned) as last_scanned FROM tracks'
|
|
);
|
|
return result[0]?.last_scanned || null;
|
|
}
|
|
|
|
/**
|
|
* Delete tracks that are no longer in the provided paths
|
|
*/
|
|
export async function deleteTracksNotInPaths(paths: string[]): Promise<void> {
|
|
if (paths.length === 0) {
|
|
const database = await initDatabase();
|
|
await database.execute('DELETE FROM tracks');
|
|
return;
|
|
}
|
|
|
|
const database = await initDatabase();
|
|
const placeholders = paths.map((_, i) => `$${i + 1}`).join(',');
|
|
await database.execute(
|
|
`DELETE FROM tracks WHERE path NOT IN (${placeholders})`,
|
|
paths
|
|
);
|
|
}
|