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
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
formatFolderPath,
|
||||
formatMp3Filename,
|
||||
sanitizeFilename,
|
||||
type Mp3Metadata,
|
||||
} from "./mp3Tags";
|
||||
import type { ScannedLibraryTrack } from "./libraryScanner";
|
||||
import type { SpotiRipSettings } from "./spotiripSettings";
|
||||
|
||||
type OrganizeDirectoryHandle = FileSystemDirectoryHandle & {
|
||||
getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<FileSystemDirectoryHandle>;
|
||||
getFileHandle(name: string, options?: { create?: boolean }): Promise<FileSystemFileHandle>;
|
||||
};
|
||||
|
||||
function segment(value: string | undefined, fallback: string) {
|
||||
return sanitizeFilename(value?.trim() || fallback).replace(/[./\\]+/g, "_");
|
||||
}
|
||||
|
||||
async function ensureDirectory(root: FileSystemDirectoryHandle, names: string[]) {
|
||||
let current = root as OrganizeDirectoryHandle;
|
||||
for (const name of names) {
|
||||
current = (await current.getDirectoryHandle(name, { create: true })) as OrganizeDirectoryHandle;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
async function exists(directory: FileSystemDirectoryHandle, name: string) {
|
||||
try {
|
||||
await (directory as OrganizeDirectoryHandle).getFileHandle(name);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function uniqueName(directory: FileSystemDirectoryHandle, desired: string) {
|
||||
if (!(await exists(directory, desired))) return desired;
|
||||
const dot = desired.lastIndexOf(".");
|
||||
const base = dot > 0 ? desired.slice(0, dot) : desired;
|
||||
const ext = dot > 0 ? desired.slice(dot) : "";
|
||||
for (let index = 2; index < 10000; index += 1) {
|
||||
const candidate = base + " (" + index + ")" + ext;
|
||||
if (!(await exists(directory, candidate))) return candidate;
|
||||
}
|
||||
throw new Error("Could not create a unique library filename.");
|
||||
}
|
||||
|
||||
function metadataForTrack(
|
||||
track: ScannedLibraryTrack,
|
||||
settings?: Pick<SpotiRipSettings, "multiDiscMode">,
|
||||
): Mp3Metadata {
|
||||
const trackLabel = track.trackNumber
|
||||
? settings?.multiDiscMode === "offset" && track.discNumber
|
||||
? String(track.discNumber * 100 + track.trackNumber)
|
||||
: settings?.multiDiscMode === "disc-prefix" && track.discNumber
|
||||
? String(track.discNumber) + "-" + String(track.trackNumber).padStart(2, "0")
|
||||
: String(track.trackNumber).padStart(2, "0")
|
||||
: "";
|
||||
return {
|
||||
title: track.title || "Unknown Track",
|
||||
artist: track.artist || "Unknown Artist",
|
||||
album: track.album,
|
||||
albumArtist: track.albumArtist,
|
||||
year: track.date?.slice(0, 4),
|
||||
trackNumber: track.trackNumber,
|
||||
trackLabel,
|
||||
discNumber: track.discNumber,
|
||||
genre: track.genre,
|
||||
isrc: track.isrc,
|
||||
};
|
||||
}
|
||||
|
||||
export function suggestedLibraryPath(
|
||||
track: ScannedLibraryTrack,
|
||||
settings?: Pick<
|
||||
SpotiRipSettings,
|
||||
"folderTemplate" | "filenameTemplate" | "multiDiscMode"
|
||||
>,
|
||||
) {
|
||||
const metadata = metadataForTrack(track, settings);
|
||||
const folders = formatFolderPath(
|
||||
settings?.folderTemplate || "{album_artist}/{year} - {album}",
|
||||
metadata,
|
||||
);
|
||||
const extension = track.file.name.includes(".")
|
||||
? track.file.name.slice(track.file.name.lastIndexOf(".")).toLowerCase()
|
||||
: "";
|
||||
const formatted = formatMp3Filename(
|
||||
settings?.filenameTemplate || "{artist} - {title}",
|
||||
metadata,
|
||||
).replace(/\.mp3$/i, "");
|
||||
const filename = segment(formatted, "Unknown Track") + extension;
|
||||
return { folders, filename };
|
||||
}
|
||||
|
||||
export async function copyOrganizedTrack(
|
||||
root: FileSystemDirectoryHandle,
|
||||
track: ScannedLibraryTrack,
|
||||
settings?: Pick<
|
||||
SpotiRipSettings,
|
||||
"folderTemplate" | "filenameTemplate" | "multiDiscMode"
|
||||
>,
|
||||
) {
|
||||
const target = suggestedLibraryPath(track, settings);
|
||||
const directory = await ensureDirectory(root, target.folders);
|
||||
const filename = await uniqueName(directory, target.filename);
|
||||
const handle = await (directory as OrganizeDirectoryHandle).getFileHandle(filename, { create: true });
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(track.file);
|
||||
await writable.close();
|
||||
return [...target.folders, filename].join("/");
|
||||
}
|
||||
Reference in New Issue
Block a user