Spaces:
Sleeping
Sleeping
| /** | |
| * Persistent mock database for development/testing | |
| * Saves to JSON file so data persists across restarts | |
| */ | |
| import { nanoid } from "nanoid"; | |
| import fs from "fs"; | |
| import path from "path"; | |
| import { fileURLToPath } from "url"; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| // Use HuggingFace Spaces persistent storage if available (/data is persistent) | |
| // Otherwise use local data directory | |
| function getDbPath(): string { | |
| const hfPersistentPath = "/data/mock_db.json"; | |
| const localPath = path.resolve(__dirname, "../data/mock_db.json"); | |
| // Check if we're on HuggingFace Spaces | |
| const isHfSpaces = process.env.SPACE_ID || process.env.SPACE_AUTHOR_NAME; | |
| if (isHfSpaces) { | |
| console.log("[MockDB] Running on HuggingFace Spaces"); | |
| // Check if /data exists and is writable | |
| if (fs.existsSync("/data")) { | |
| try { | |
| fs.accessSync("/data", fs.constants.W_OK); | |
| console.log("[MockDB] Using persistent storage: /data/mock_db.json"); | |
| return hfPersistentPath; | |
| } catch (e) { | |
| console.log("[MockDB] /data exists but not writable, using /app/data"); | |
| console.log("[MockDB] NOTE: Data will NOT persist across restarts!"); | |
| console.log("[MockDB] Enable persistent storage in Space settings for data persistence."); | |
| } | |
| } else { | |
| console.log("[MockDB] /data directory not found, using /app/data"); | |
| console.log("[MockDB] NOTE: Data will NOT persist across restarts!"); | |
| } | |
| } | |
| return localPath; | |
| } | |
| const DB_FILE = getDbPath(); | |
| interface MockUser { | |
| id: number; | |
| openId: string; | |
| name: string | null; | |
| email: string | null; | |
| loginMethod: string | null; | |
| role: "user" | "admin"; | |
| createdAt: Date; | |
| updatedAt: Date; | |
| lastSignedIn: Date; | |
| } | |
| interface MockTrack { | |
| id: number; | |
| userId: number | null; | |
| title: string; | |
| artist: string | null; | |
| trackType: "ai_generated" | "training_reference"; | |
| fileKey: string; | |
| fileUrl: string; | |
| fileSize: number | null; | |
| mimeType: string | null; | |
| duration: number | null; | |
| status: "pending" | "processing" | "completed" | "failed"; | |
| errorMessage: string | null; | |
| createdAt: Date; | |
| updatedAt: Date; | |
| } | |
| interface MockStem { | |
| id: number; | |
| trackId: number; | |
| stemType: "vocals" | "drums" | "bass" | "other"; | |
| fileKey: string; | |
| fileUrl: string; | |
| duration: number | null; | |
| createdAt: Date; | |
| } | |
| interface MockFingerprint { | |
| id: number; | |
| stemId: number; | |
| algorithm: string; | |
| fingerprintData: string; | |
| version: string | null; | |
| createdAt: Date; | |
| } | |
| interface MockEmbedding { | |
| id: number; | |
| stemId: number; | |
| model: string; | |
| embeddingVector: number[]; | |
| dimension: number; | |
| createdAt: Date; | |
| } | |
| interface MockAttributionResult { | |
| id: number; | |
| aiTrackId: number; | |
| aiStemId: number | null; | |
| trainingTrackId: number; | |
| trainingStemId: number | null; | |
| method: string; | |
| score: number; | |
| confidence: number | null; | |
| metadata: Record<string, unknown> | null; | |
| createdAt: Date; | |
| } | |
| interface MockProcessingJob { | |
| id: number; | |
| trackId: number; | |
| jobType: "stem_separation" | "fingerprinting" | "embedding" | "attribution"; | |
| status: "pending" | "running" | "completed" | "failed"; | |
| progress: number; | |
| errorMessage: string | null; | |
| resultData: Record<string, unknown> | null; | |
| startedAt: Date | null; | |
| completedAt: Date | null; | |
| createdAt: Date; | |
| } | |
| // Persistent storage with JSON file backup | |
| class MockDatabase { | |
| private users = new Map<number, MockUser>(); | |
| private tracks = new Map<number, MockTrack>(); | |
| private stems = new Map<number, MockStem>(); | |
| private fingerprints = new Map<number, MockFingerprint>(); | |
| private embeddings = new Map<number, MockEmbedding>(); | |
| private attributionResults = new Map<number, MockAttributionResult>(); | |
| private processingJobs = new Map<number, MockProcessingJob>(); | |
| private nextUserId = 1; | |
| private nextTrackId = 1; | |
| private nextStemId = 1; | |
| private nextFingerprintId = 1; | |
| private nextEmbeddingId = 1; | |
| private nextAttributionId = 1; | |
| private nextJobId = 1; | |
| constructor() { | |
| this.load(); | |
| } | |
| private load(): void { | |
| try { | |
| if (fs.existsSync(DB_FILE)) { | |
| const data = JSON.parse(fs.readFileSync(DB_FILE, 'utf-8')); | |
| // Restore maps with date parsing | |
| const parseDate = (obj: Record<string, unknown>) => { | |
| for (const key of Object.keys(obj)) { | |
| if (typeof obj[key] === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(obj[key] as string)) { | |
| obj[key] = new Date(obj[key] as string); | |
| } | |
| } | |
| return obj; | |
| }; | |
| if (data.users) { | |
| for (const u of data.users) { | |
| this.users.set(u.id, parseDate(u) as MockUser); | |
| } | |
| } | |
| if (data.tracks) { | |
| for (const t of data.tracks) { | |
| this.tracks.set(t.id, parseDate(t) as MockTrack); | |
| } | |
| } | |
| if (data.stems) { | |
| for (const s of data.stems) { | |
| this.stems.set(s.id, parseDate(s) as MockStem); | |
| } | |
| } | |
| if (data.fingerprints) { | |
| for (const f of data.fingerprints) { | |
| this.fingerprints.set(f.id, parseDate(f) as MockFingerprint); | |
| } | |
| } | |
| if (data.embeddings) { | |
| for (const e of data.embeddings) { | |
| this.embeddings.set(e.id, parseDate(e) as MockEmbedding); | |
| } | |
| } | |
| if (data.attributionResults) { | |
| for (const a of data.attributionResults) { | |
| this.attributionResults.set(a.id, parseDate(a) as MockAttributionResult); | |
| } | |
| } | |
| if (data.processingJobs) { | |
| for (const j of data.processingJobs) { | |
| this.processingJobs.set(j.id, parseDate(j) as MockProcessingJob); | |
| } | |
| } | |
| // Restore next IDs | |
| this.nextUserId = data.nextUserId || 1; | |
| this.nextTrackId = data.nextTrackId || 1; | |
| this.nextStemId = data.nextStemId || 1; | |
| this.nextFingerprintId = data.nextFingerprintId || 1; | |
| this.nextEmbeddingId = data.nextEmbeddingId || 1; | |
| this.nextAttributionId = data.nextAttributionId || 1; | |
| this.nextJobId = data.nextJobId || 1; | |
| console.log(`[MockDB] Loaded ${this.tracks.size} tracks, ${this.users.size} users from ${DB_FILE}`); | |
| } | |
| } catch (error) { | |
| console.error('[MockDB] Failed to load database:', error); | |
| } | |
| } | |
| private save(): void { | |
| try { | |
| // Ensure directory exists | |
| const dir = path.dirname(DB_FILE); | |
| if (!fs.existsSync(dir)) { | |
| fs.mkdirSync(dir, { recursive: true }); | |
| } | |
| const data = { | |
| users: Array.from(this.users.values()), | |
| tracks: Array.from(this.tracks.values()), | |
| stems: Array.from(this.stems.values()), | |
| fingerprints: Array.from(this.fingerprints.values()), | |
| embeddings: Array.from(this.embeddings.values()), | |
| attributionResults: Array.from(this.attributionResults.values()), | |
| processingJobs: Array.from(this.processingJobs.values()), | |
| nextUserId: this.nextUserId, | |
| nextTrackId: this.nextTrackId, | |
| nextStemId: this.nextStemId, | |
| nextFingerprintId: this.nextFingerprintId, | |
| nextEmbeddingId: this.nextEmbeddingId, | |
| nextAttributionId: this.nextAttributionId, | |
| nextJobId: this.nextJobId, | |
| }; | |
| fs.writeFileSync(DB_FILE, JSON.stringify(data, null, 2)); | |
| } catch (error) { | |
| console.error('[MockDB] Failed to save database:', error); | |
| } | |
| } | |
| // User operations | |
| upsertUser(user: Partial<MockUser> & { openId: string }): MockUser { | |
| // Find existing user by openId | |
| let existing: MockUser | undefined; | |
| for (const u of this.users.values()) { | |
| if (u.openId === user.openId) { | |
| existing = u; | |
| break; | |
| } | |
| } | |
| if (existing) { | |
| const updated = { | |
| ...existing, | |
| ...user, | |
| updatedAt: new Date(), | |
| lastSignedIn: user.lastSignedIn || new Date(), | |
| }; | |
| this.users.set(existing.id, updated); | |
| this.save(); | |
| return updated; | |
| } | |
| const newUser: MockUser = { | |
| id: this.nextUserId++, | |
| openId: user.openId, | |
| name: user.name || null, | |
| email: user.email || null, | |
| loginMethod: user.loginMethod || null, | |
| role: user.role || "user", | |
| createdAt: new Date(), | |
| updatedAt: new Date(), | |
| lastSignedIn: user.lastSignedIn || new Date(), | |
| }; | |
| this.users.set(newUser.id, newUser); | |
| this.save(); | |
| return newUser; | |
| } | |
| getUserByOpenId(openId: string): MockUser | undefined { | |
| for (const user of this.users.values()) { | |
| if (user.openId === openId) { | |
| return user; | |
| } | |
| } | |
| return undefined; | |
| } | |
| // Track operations | |
| createTrack(track: Omit<MockTrack, "id" | "createdAt" | "updatedAt">): number { | |
| const id = this.nextTrackId++; | |
| const newTrack: MockTrack = { | |
| id, | |
| ...track, | |
| createdAt: new Date(), | |
| updatedAt: new Date(), | |
| }; | |
| this.tracks.set(id, newTrack); | |
| this.save(); | |
| return id; | |
| } | |
| getTrackById(id: number): MockTrack | undefined { | |
| return this.tracks.get(id); | |
| } | |
| getUserTracks(userId: number): MockTrack[] { | |
| return Array.from(this.tracks.values()) | |
| .filter(t => t.userId === userId) | |
| .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); | |
| } | |
| updateTrackStatus(id: number, status: MockTrack["status"], errorMessage?: string): void { | |
| const track = this.tracks.get(id); | |
| if (track) { | |
| track.status = status; | |
| if (errorMessage !== undefined) track.errorMessage = errorMessage; | |
| track.updatedAt = new Date(); | |
| this.save(); | |
| } | |
| } | |
| getTrainingTracks(): MockTrack[] { | |
| return Array.from(this.tracks.values()) | |
| .filter(t => t.trackType === "training_reference"); | |
| } | |
| // Stem operations | |
| createStem(stem: Omit<MockStem, "id" | "createdAt">): number { | |
| const id = this.nextStemId++; | |
| const newStem: MockStem = { | |
| id, | |
| ...stem, | |
| createdAt: new Date(), | |
| }; | |
| this.stems.set(id, newStem); | |
| this.save(); | |
| return id; | |
| } | |
| getTrackStems(trackId: number): MockStem[] { | |
| return Array.from(this.stems.values()).filter(s => s.trackId === trackId); | |
| } | |
| // Fingerprint operations | |
| createFingerprint(fp: Omit<MockFingerprint, "id" | "createdAt">): number { | |
| const id = this.nextFingerprintId++; | |
| const newFp: MockFingerprint = { | |
| id, | |
| ...fp, | |
| createdAt: new Date(), | |
| }; | |
| this.fingerprints.set(id, newFp); | |
| this.save(); | |
| return id; | |
| } | |
| // Embedding operations | |
| createEmbedding(emb: Omit<MockEmbedding, "id" | "createdAt">): number { | |
| const id = this.nextEmbeddingId++; | |
| const newEmb: MockEmbedding = { | |
| id, | |
| ...emb, | |
| createdAt: new Date(), | |
| }; | |
| this.embeddings.set(id, newEmb); | |
| this.save(); | |
| return id; | |
| } | |
| // Attribution operations | |
| createAttributionResult(attr: Omit<MockAttributionResult, "id" | "createdAt">): number { | |
| const id = this.nextAttributionId++; | |
| const newAttr: MockAttributionResult = { | |
| id, | |
| ...attr, | |
| createdAt: new Date(), | |
| }; | |
| this.attributionResults.set(id, newAttr); | |
| this.save(); | |
| return id; | |
| } | |
| getTrackAttributions(aiTrackId: number): Array<MockAttributionResult & { trainingTrack?: MockTrack }> { | |
| return Array.from(this.attributionResults.values()) | |
| .filter(a => a.aiTrackId === aiTrackId) | |
| .map(a => ({ | |
| ...a, | |
| trainingTrack: this.tracks.get(a.trainingTrackId), | |
| })) | |
| .sort((a, b) => b.score - a.score); | |
| } | |
| clearTrackAttributions(aiTrackId: number): number { | |
| let count = 0; | |
| for (const [id, attr] of this.attributionResults.entries()) { | |
| if (attr.aiTrackId === aiTrackId) { | |
| this.attributionResults.delete(id); | |
| count++; | |
| } | |
| } | |
| if (count > 0) { | |
| this.save(); | |
| } | |
| return count; | |
| } | |
| // Processing job operations | |
| createProcessingJob(job: Omit<MockProcessingJob, "id" | "createdAt">): number { | |
| const id = this.nextJobId++; | |
| const newJob: MockProcessingJob = { | |
| id, | |
| ...job, | |
| createdAt: new Date(), | |
| }; | |
| this.processingJobs.set(id, newJob); | |
| this.save(); | |
| return id; | |
| } | |
| updateProcessingJob(id: number, updates: Partial<MockProcessingJob>): void { | |
| const job = this.processingJobs.get(id); | |
| if (job) { | |
| Object.assign(job, updates); | |
| this.save(); | |
| } | |
| } | |
| getTrackJobs(trackId: number): MockProcessingJob[] { | |
| return Array.from(this.processingJobs.values()) | |
| .filter(j => j.trackId === trackId) | |
| .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); | |
| } | |
| } | |
| // Singleton instance | |
| export const mockDb = new MockDatabase(); | |
| // Export type-compatible functions for use by the app | |
| export const isMockDb = true; | |