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 { if (!db) { db = await Database.load('sqlite:library.db'); } return db; } /** * Get all artists with their albums */ export async function getArtistsWithAlbums(): Promise { const database = await initDatabase(); const artists = await database.select( 'SELECT * FROM artists ORDER BY name COLLATE NOCASE' ); return artists; } /** * Get all albums for a specific artist */ export async function getAlbumsByArtist(artistId: number): Promise { const database = await initDatabase(); const albums = await database.select( '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 { const database = await initDatabase(); const albums = await database.select( '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 { const database = await initDatabase(); const albums = await database.select( '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 { 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( '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 { 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 { 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 { 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 { 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 { const database = await initDatabase(); const tracks = await database.select( '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 { 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 { 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 { 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 ); }