Files
R3ap3r 3178b8f4f7 Aurora SpotiRip desktop and web app
- 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
2026-09-25 23:20:57 -05:00

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();
}
}