Add library manager with tracks, albums and uploads sections

This commit is contained in:
2026-09-25 01:07:15 +02:00
parent 4ac73d8fa3
commit 9928840577
28 changed files with 1622 additions and 118 deletions
+98
View File
@@ -0,0 +1,98 @@
export const UPLOAD_MAX_BYTES = 500 * 1024 * 1024;
export const UPLOAD_FORMATS = ['wav', 'flac', 'aiff', 'aif', 'mp3'];
export type Visibility = 'private' | 'public';
export interface QueuedUpload {
id: string;
file: File;
title: string;
format: string;
error: string | null;
duration: number | null;
}
export function fileExtension(name: string): string {
const dot = name.lastIndexOf('.');
return dot < 0 ? '' : name.slice(dot + 1).toLowerCase();
}
export function titleFromFileName(name: string): string {
const base = name.replace(/\.[^.]+$/, '');
const cleaned = base
.replace(/^\d{1,3}\s*[-_.)]\s*/, '')
.replace(/[_]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (cleaned === '') return base;
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
}
export function uploadError(file: File): string | null {
if (!UPLOAD_FORMATS.includes(fileExtension(file.name))) return 'Formato no compatible';
if (file.size > UPLOAD_MAX_BYTES) return 'Supera 500 MB';
if (file.size === 0) return 'Archivo vacío';
return null;
}
export function toQueuedUpload(file: File): QueuedUpload {
return {
id: crypto.randomUUID(),
file,
title: titleFromFileName(file.name),
format: fileExtension(file.name).toUpperCase() || '?',
error: uploadError(file),
duration: null
};
}
export function summarizeQueue(items: QueuedUpload[]): { ready: number; failed: number } {
const failed = items.filter((item) => item.error !== null).length;
return { ready: items.length - failed, failed };
}
async function readEntries(reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> {
const all: FileSystemEntry[] = [];
for (;;) {
const batch = await new Promise<FileSystemEntry[]>((resolve, reject) =>
reader.readEntries(resolve, reject)
);
if (batch.length === 0) return all;
all.push(...batch);
}
}
async function filesFromEntry(entry: FileSystemEntry): Promise<File[]> {
if (entry.isFile) {
const file = await new Promise<File>((resolve, reject) =>
(entry as FileSystemFileEntry).file(resolve, reject)
);
return [file];
}
if (!entry.isDirectory) return [];
const children = await readEntries((entry as FileSystemDirectoryEntry).createReader());
return (await Promise.all(children.map(filesFromEntry))).flat();
}
export async function filesFromDrop(transfer: DataTransfer): Promise<File[]> {
const entries = Array.from(transfer.items)
.map((item) => item.webkitGetAsEntry?.() ?? null)
.filter((entry): entry is FileSystemEntry => entry !== null);
if (entries.length === 0) return Array.from(transfer.files);
return (await Promise.all(entries.map(filesFromEntry))).flat();
}
export function readDuration(file: File): Promise<number | null> {
return new Promise((resolve) => {
const url = URL.createObjectURL(file);
const audio = new Audio();
const done = (value: number | null) => {
URL.revokeObjectURL(url);
resolve(value);
};
audio.preload = 'metadata';
audio.onloadedmetadata = () => done(Number.isFinite(audio.duration) ? audio.duration : null);
audio.onerror = () => done(null);
audio.src = url;
});
}