// Сбор файлов из drag-and-drop и выбора папки с относительными путями (они подсказывают альбом, docs/uploads.md#5).
export interface PickedFile {
file: File;
relPath: string; // «Кино/1988 - Группа крови/01 - Группа крови.flac» или просто имя файла
}
/** Форматы, которые принимает очередь (сервер всё равно проверяет содержимое через ffprobe). */
export const AUDIO_EXTENSIONS = ['flac', 'mp3', 'm4a', 'ogg', 'opus', 'wav', 'aiff', 'aif', 'wv'];
export function isAudio(name: string): boolean {
const ext = name.split('.').pop()?.toLowerCase() ?? '';
return AUDIO_EXTENSIONS.includes(ext);
}
/** Файлы из (в том числе webkitdirectory — тогда путь есть в webkitRelativePath). */
export function fromInput(list: FileList | null): PickedFile[] {
return [...(list ?? [])].map((file) => ({ file, relPath: file.webkitRelativePath || file.name }));
}
/** Файлы из перетаскивания: папки обходятся рекурсивно (FileSystemEntry API). */
export async function fromDrop(dt: DataTransfer): Promise {
const entries = [...dt.items]
.filter((it) => it.kind === 'file')
.map((it) => it.webkitGetAsEntry?.())
.filter((e): e is FileSystemEntry => !!e);
if (!entries.length) return fromInput(dt.files);
const out: PickedFile[] = [];
for (const e of entries) await walk(e, '', out);
return out;
}
async function walk(entry: FileSystemEntry, prefix: string, out: PickedFile[]) {
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isFile) {
const file = await new Promise((resolve, reject) => (entry as FileSystemFileEntry).file(resolve, reject));
out.push({ file, relPath: path });
return;
}
if (!entry.isDirectory || entry.name.startsWith('.')) return;
const reader = (entry as FileSystemDirectoryEntry).createReader();
// readEntries отдаёт порциями (~100), пока не вернёт пустой массив.
for (;;) {
const batch = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
if (!batch.length) break;
for (const child of batch) await walk(child, path, out);
}
}