mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
Combines LightWrite (beat/lyrics editor) and Mukke (iOS music player) into a single web-based music workspace app. Archives the old Mukke mobile app. - Rename: @lightwrite/* → @mukke/*, all branding, configs, Dockerfiles - New DB schemas: songs, playlists, playlist_songs + songId FK on projects - New backend modules: SongModule, PlaylistModule, LibraryModule - New web: app shell with sidebar, library (songs/albums/artists/genres), web player (queue/shuffle/repeat/MediaSession), playlists, search, upload, dashboard, album/artist/genre detail pages - Auth: add forgot-password + reset-password pages, extend auth store - Tests: 40 backend unit tests (song, playlist, library services) - Config: env generation, MinIO bucket, docker-compose prod, cloudflare - Docs: update CLAUDE.md, auth guidelines with SvelteKit checklist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
102 lines
3 KiB
TypeScript
102 lines
3 KiB
TypeScript
import * as FileSystem from 'expo-file-system';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
const MUSIC_DIR = `${FileSystem.documentDirectory}music/`;
|
|
const ARTWORK_DIR = `${FileSystem.documentDirectory}artwork/`;
|
|
|
|
export async function ensureDirectories(): Promise<void> {
|
|
const musicInfo = await FileSystem.getInfoAsync(MUSIC_DIR);
|
|
if (!musicInfo.exists) {
|
|
await FileSystem.makeDirectoryAsync(MUSIC_DIR, { intermediates: true });
|
|
}
|
|
|
|
const artworkInfo = await FileSystem.getInfoAsync(ARTWORK_DIR);
|
|
if (!artworkInfo.exists) {
|
|
await FileSystem.makeDirectoryAsync(ARTWORK_DIR, { intermediates: true });
|
|
}
|
|
}
|
|
|
|
export function getFileExtension(uri: string): string {
|
|
const parts = uri.split('.');
|
|
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : 'mp3';
|
|
}
|
|
|
|
export async function copyToMusicDirectory(
|
|
sourceUri: string
|
|
): Promise<{ path: string; id: string }> {
|
|
await ensureDirectories();
|
|
const id = uuidv4();
|
|
const ext = getFileExtension(sourceUri);
|
|
const destPath = `${MUSIC_DIR}${id}.${ext}`;
|
|
await FileSystem.copyAsync({ from: sourceUri, to: destPath });
|
|
return { path: destPath, id };
|
|
}
|
|
|
|
export async function saveArtwork(data: Uint8Array, songId: string): Promise<string> {
|
|
await ensureDirectories();
|
|
const artworkPath = `${ARTWORK_DIR}${songId}.jpg`;
|
|
const base64 = uint8ArrayToBase64(data);
|
|
await FileSystem.writeAsStringAsync(artworkPath, base64, {
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
});
|
|
return artworkPath;
|
|
}
|
|
|
|
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
let binary = '';
|
|
for (let i = 0; i < bytes.length; i++) {
|
|
binary += String.fromCharCode(bytes[i]);
|
|
}
|
|
return btoa(binary);
|
|
}
|
|
|
|
export async function deleteFile(path: string): Promise<void> {
|
|
const info = await FileSystem.getInfoAsync(path);
|
|
if (info.exists) {
|
|
await FileSystem.deleteAsync(path);
|
|
}
|
|
}
|
|
|
|
export async function getStorageInfo(): Promise<{
|
|
musicSize: number;
|
|
artworkSize: number;
|
|
totalFiles: number;
|
|
}> {
|
|
let musicSize = 0;
|
|
let artworkSize = 0;
|
|
let totalFiles = 0;
|
|
|
|
try {
|
|
const musicFiles = await FileSystem.readDirectoryAsync(MUSIC_DIR);
|
|
for (const file of musicFiles) {
|
|
const info = await FileSystem.getInfoAsync(`${MUSIC_DIR}${file}`);
|
|
if (info.exists && !info.isDirectory && 'size' in info) {
|
|
musicSize += info.size ?? 0;
|
|
totalFiles++;
|
|
}
|
|
}
|
|
} catch {
|
|
// Directory might not exist yet
|
|
}
|
|
|
|
try {
|
|
const artworkFiles = await FileSystem.readDirectoryAsync(ARTWORK_DIR);
|
|
for (const file of artworkFiles) {
|
|
const info = await FileSystem.getInfoAsync(`${ARTWORK_DIR}${file}`);
|
|
if (info.exists && !info.isDirectory && 'size' in info) {
|
|
artworkSize += info.size ?? 0;
|
|
}
|
|
}
|
|
} catch {
|
|
// Directory might not exist yet
|
|
}
|
|
|
|
return { musicSize, artworkSize, totalFiles };
|
|
}
|
|
|
|
export function formatFileSize(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
}
|