- Spotify playlist, library, album and artist browsing with in-app playback - Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks - Electron desktop app (castLabs Widevine) with a permanent sidebar layout - Linux and Windows packaging scripts
123 lines
3.5 KiB
TypeScript
123 lines
3.5 KiB
TypeScript
export type LibraryTrackRecord = {
|
|
id: string;
|
|
isrc?: string;
|
|
title: string;
|
|
artist: string;
|
|
album?: string;
|
|
albumArtist?: string;
|
|
savedPath?: string;
|
|
artworkUrl?: string;
|
|
source?: string;
|
|
addedAt: number;
|
|
};
|
|
|
|
const DB_NAME = "aurora-spotirip-library";
|
|
const TRACK_STORE = "tracks";
|
|
const HISTORY_STORE = "history";
|
|
|
|
function normalize(value: string) {
|
|
return value
|
|
.normalize("NFKD")
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
export function trackIdentity(track: {
|
|
isrc?: string;
|
|
artist: string;
|
|
title: string;
|
|
}) {
|
|
return track.isrc?.trim().toUpperCase() ||
|
|
`${normalize(track.artist)}::${normalize(track.title)}`;
|
|
}
|
|
|
|
function openDb() {
|
|
return new Promise<IDBDatabase>((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, 1);
|
|
request.onupgradeneeded = () => {
|
|
const db = request.result;
|
|
if (!db.objectStoreNames.contains(TRACK_STORE)) {
|
|
db.createObjectStore(TRACK_STORE, { keyPath: "id" });
|
|
}
|
|
if (!db.objectStoreNames.contains(HISTORY_STORE)) {
|
|
db.createObjectStore(HISTORY_STORE, { keyPath: "id" });
|
|
}
|
|
};
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error || new Error("Unable to open library index."));
|
|
});
|
|
}
|
|
|
|
export async function findLibraryDuplicate(track: {
|
|
isrc?: string;
|
|
artist: string;
|
|
title: string;
|
|
}) {
|
|
if (typeof indexedDB === "undefined") return undefined;
|
|
const db = await openDb();
|
|
try {
|
|
const id = trackIdentity(track);
|
|
return await new Promise<LibraryTrackRecord | undefined>((resolve, reject) => {
|
|
const tx = db.transaction(TRACK_STORE, "readonly");
|
|
const request = tx.objectStore(TRACK_STORE).get(id);
|
|
request.onsuccess = () => resolve(request.result as LibraryTrackRecord | undefined);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export async function rememberLibraryTrack(
|
|
track: Omit<LibraryTrackRecord, "id" | "addedAt">,
|
|
) {
|
|
const db = await openDb();
|
|
try {
|
|
const record: LibraryTrackRecord = {
|
|
...track,
|
|
id: trackIdentity(track),
|
|
addedAt: Date.now(),
|
|
};
|
|
await new Promise<void>((resolve, reject) => {
|
|
const tx = db.transaction([TRACK_STORE, HISTORY_STORE], "readwrite");
|
|
tx.objectStore(TRACK_STORE).put(record);
|
|
tx.objectStore(HISTORY_STORE).put(record);
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error || new Error("Unable to update library history."));
|
|
});
|
|
return record;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export async function listLibraryHistory() {
|
|
if (typeof indexedDB === "undefined") return [] as LibraryTrackRecord[];
|
|
const db = await openDb();
|
|
try {
|
|
const records = await new Promise<LibraryTrackRecord[]>((resolve, reject) => {
|
|
const tx = db.transaction(HISTORY_STORE, "readonly");
|
|
const request = tx.objectStore(HISTORY_STORE).getAll();
|
|
request.onsuccess = () => resolve((request.result || []) as LibraryTrackRecord[]);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
return records.sort((a, b) => b.addedAt - a.addedAt);
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
export async function clearLibraryHistory() {
|
|
const db = await openDb();
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const tx = db.transaction(HISTORY_STORE, "readwrite");
|
|
tx.objectStore(HISTORY_STORE).clear();
|
|
tx.oncomplete = () => resolve();
|
|
tx.onerror = () => reject(tx.error);
|
|
});
|
|
} finally {
|
|
db.close();
|
|
}
|
|
} |