chore(refactor): remove unused files (TwoPhaseSyncService, unused frontend types)
This commit is contained in:
parent
f6ecd07d98
commit
940469ba52
@ -1,275 +0,0 @@
|
|||||||
import { S3Service } from './s3Service.js';
|
|
||||||
import { AudioMetadataService } from './audioMetadataService.js';
|
|
||||||
import { SongMatchingService } from './songMatchingService.js';
|
|
||||||
import { MusicFile } from '../models/MusicFile.js';
|
|
||||||
import { Song } from '../models/Song.js';
|
|
||||||
|
|
||||||
export interface SyncResult {
|
|
||||||
phase1: {
|
|
||||||
totalFiles: number;
|
|
||||||
quickMatches: number;
|
|
||||||
unmatchedFiles: number;
|
|
||||||
errors: number;
|
|
||||||
};
|
|
||||||
phase2: {
|
|
||||||
processedFiles: number;
|
|
||||||
complexMatches: number;
|
|
||||||
stillUnmatched: number;
|
|
||||||
errors: number;
|
|
||||||
};
|
|
||||||
total: {
|
|
||||||
processed: number;
|
|
||||||
matched: number;
|
|
||||||
unmatched: number;
|
|
||||||
errors: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TwoPhaseSyncService {
|
|
||||||
private s3Service: S3Service;
|
|
||||||
private audioMetadataService: AudioMetadataService;
|
|
||||||
private songMatchingService: SongMatchingService;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.audioMetadataService = new AudioMetadataService();
|
|
||||||
this.songMatchingService = new SongMatchingService();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize S3 service
|
|
||||||
*/
|
|
||||||
async initialize(): Promise<void> {
|
|
||||||
this.s3Service = await S3Service.createFromConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract filename from S3 key
|
|
||||||
*/
|
|
||||||
private getFilenameFromS3Key(s3Key: string): string {
|
|
||||||
return s3Key.split('/').pop() || s3Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract filename from Rekordbox location path
|
|
||||||
*/
|
|
||||||
private getFilenameFromLocation(location: string): string {
|
|
||||||
// Handle both Windows and Unix paths
|
|
||||||
const normalizedPath = location.replace(/\\/g, '/');
|
|
||||||
return normalizedPath.split('/').pop() || location;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize filename for comparison (remove extension, lowercase)
|
|
||||||
*/
|
|
||||||
private normalizeFilename(filename: string): string {
|
|
||||||
return filename.replace(/\.[^/.]+$/, '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Phase 1: Quick filename-based matching
|
|
||||||
*/
|
|
||||||
async phase1QuickMatch(): Promise<{
|
|
||||||
quickMatches: MusicFile[];
|
|
||||||
unmatchedFiles: any[];
|
|
||||||
errors: any[];
|
|
||||||
}> {
|
|
||||||
console.log('🚀 Starting Phase 1: Quick filename-based matching...');
|
|
||||||
|
|
||||||
// Get all S3 files
|
|
||||||
const s3Files = await this.s3Service.listAllFiles();
|
|
||||||
const audioFiles = s3Files.filter(s3File => {
|
|
||||||
const filename = this.getFilenameFromS3Key(s3File.key);
|
|
||||||
return this.audioMetadataService.isAudioFile(filename);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📁 Found ${audioFiles.length} audio files in S3`);
|
|
||||||
|
|
||||||
// Get existing music files to avoid duplicates
|
|
||||||
const existingFiles = await MusicFile.find({}, { s3Key: 1 });
|
|
||||||
const existingS3Keys = new Set(existingFiles.map(f => f.s3Key));
|
|
||||||
const newAudioFiles = audioFiles.filter(s3File => !existingS3Keys.has(s3File.key));
|
|
||||||
|
|
||||||
console.log(`🆕 Found ${newAudioFiles.length} new audio files to process`);
|
|
||||||
|
|
||||||
// Get all songs from database for filename matching
|
|
||||||
const allSongs = await Song.find({}, { id: 1, title: 1, artist: 1, location: 1 });
|
|
||||||
console.log(`🎵 Found ${allSongs.length} songs in database for matching`);
|
|
||||||
|
|
||||||
const quickMatches: MusicFile[] = [];
|
|
||||||
const unmatchedFiles: any[] = [];
|
|
||||||
const errors: any[] = [];
|
|
||||||
|
|
||||||
for (const s3File of newAudioFiles) {
|
|
||||||
try {
|
|
||||||
const s3Filename = this.getFilenameFromS3Key(s3File.key);
|
|
||||||
const normalizedS3Filename = this.normalizeFilename(s3Filename);
|
|
||||||
|
|
||||||
// Try to find exact filename match in Rekordbox songs
|
|
||||||
let matchedSong = null;
|
|
||||||
|
|
||||||
for (const song of allSongs) {
|
|
||||||
if (song.location) {
|
|
||||||
const rekordboxFilename = this.getFilenameFromLocation(song.location);
|
|
||||||
const normalizedRekordboxFilename = this.normalizeFilename(rekordboxFilename);
|
|
||||||
|
|
||||||
if (normalizedS3Filename === normalizedRekordboxFilename) {
|
|
||||||
matchedSong = song;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedSong) {
|
|
||||||
console.log(`✅ Quick match found: ${s3Filename} -> ${matchedSong.title}`);
|
|
||||||
|
|
||||||
// Extract basic metadata from filename (no need to download file)
|
|
||||||
const basicMetadata = this.audioMetadataService.extractBasicMetadataFromFilename(s3Filename);
|
|
||||||
|
|
||||||
const musicFile = new MusicFile({
|
|
||||||
originalName: s3Filename,
|
|
||||||
s3Key: s3File.key,
|
|
||||||
s3Url: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET_NAME}/${s3File.key}`,
|
|
||||||
contentType: 'audio/mpeg',
|
|
||||||
size: s3File.size,
|
|
||||||
...basicMetadata,
|
|
||||||
songId: matchedSong._id, // Link to the matched song
|
|
||||||
});
|
|
||||||
|
|
||||||
quickMatches.push(musicFile);
|
|
||||||
} else {
|
|
||||||
console.log(`❓ No quick match for: ${s3Filename}`);
|
|
||||||
unmatchedFiles.push(s3File);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Error processing ${s3File.key}:`, error);
|
|
||||||
errors.push({ file: s3File, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Phase 1 completed: ${quickMatches.length} quick matches, ${unmatchedFiles.length} unmatched files`);
|
|
||||||
|
|
||||||
return { quickMatches, unmatchedFiles, errors };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Phase 2: Complex matching for unmatched files
|
|
||||||
*/
|
|
||||||
async phase2ComplexMatch(unmatchedFiles: any[]): Promise<{
|
|
||||||
processedFiles: MusicFile[];
|
|
||||||
complexMatches: number;
|
|
||||||
stillUnmatched: number;
|
|
||||||
errors: any[];
|
|
||||||
}> {
|
|
||||||
console.log('🔍 Starting Phase 2: Complex matching for unmatched files...');
|
|
||||||
|
|
||||||
const processedFiles: MusicFile[] = [];
|
|
||||||
let complexMatches = 0;
|
|
||||||
let stillUnmatched = 0;
|
|
||||||
const errors: any[] = [];
|
|
||||||
|
|
||||||
for (const s3File of unmatchedFiles) {
|
|
||||||
try {
|
|
||||||
const filename = this.getFilenameFromS3Key(s3File.key);
|
|
||||||
console.log(`🔍 Processing unmatched file: ${filename}`);
|
|
||||||
|
|
||||||
// Download file and extract metadata
|
|
||||||
const fileBuffer = await this.s3Service.getFileContent(s3File.key);
|
|
||||||
const metadata = await this.audioMetadataService.extractMetadata(fileBuffer, filename);
|
|
||||||
|
|
||||||
const musicFile = new MusicFile({
|
|
||||||
originalName: filename,
|
|
||||||
s3Key: s3File.key,
|
|
||||||
s3Url: `${process.env.S3_ENDPOINT}/${process.env.S3_BUCKET_NAME}/${s3File.key}`,
|
|
||||||
contentType: 'audio/mpeg',
|
|
||||||
size: s3File.size,
|
|
||||||
...metadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
processedFiles.push(musicFile);
|
|
||||||
|
|
||||||
// Try complex matching
|
|
||||||
const matchResult = await this.songMatchingService.matchMusicFileToSongs(musicFile, {
|
|
||||||
minConfidence: 0.7,
|
|
||||||
enableFuzzyMatching: true,
|
|
||||||
enablePartialMatching: true,
|
|
||||||
maxResults: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matchResult.length > 0 && matchResult[0].confidence >= 0.7) {
|
|
||||||
const bestMatch = matchResult[0];
|
|
||||||
musicFile.songId = bestMatch.song._id;
|
|
||||||
complexMatches++;
|
|
||||||
console.log(`✅ Complex match found: ${filename} -> ${bestMatch.song.title} (${bestMatch.confidence})`);
|
|
||||||
} else {
|
|
||||||
stillUnmatched++;
|
|
||||||
console.log(`❓ No complex match for: ${filename}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Error processing ${s3File.key}:`, error);
|
|
||||||
errors.push({ file: s3File, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Phase 2 completed: ${complexMatches} complex matches, ${stillUnmatched} still unmatched`);
|
|
||||||
|
|
||||||
return { processedFiles, complexMatches, stillUnmatched, errors };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run complete two-phase sync
|
|
||||||
*/
|
|
||||||
async runTwoPhaseSync(): Promise<SyncResult> {
|
|
||||||
console.log('🎯 Starting Two-Phase S3 Sync...');
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
await this.initialize();
|
|
||||||
|
|
||||||
// Phase 1: Quick filename matching
|
|
||||||
const phase1Result = await this.phase1QuickMatch();
|
|
||||||
|
|
||||||
// Phase 2: Complex matching for unmatched files
|
|
||||||
const phase2Result = await this.phase2ComplexMatch(phase1Result.unmatchedFiles);
|
|
||||||
|
|
||||||
// Combine all music files
|
|
||||||
const allMusicFiles = [...phase1Result.quickMatches, ...phase2Result.processedFiles];
|
|
||||||
|
|
||||||
// Batch save all music files
|
|
||||||
if (allMusicFiles.length > 0) {
|
|
||||||
console.log(`💾 Saving ${allMusicFiles.length} music files to database...`);
|
|
||||||
await MusicFile.insertMany(allMusicFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
const endTime = Date.now();
|
|
||||||
const duration = ((endTime - startTime) / 1000).toFixed(2);
|
|
||||||
|
|
||||||
const result: SyncResult = {
|
|
||||||
phase1: {
|
|
||||||
totalFiles: phase1Result.quickMatches.length + phase1Result.unmatchedFiles.length,
|
|
||||||
quickMatches: phase1Result.quickMatches.length,
|
|
||||||
unmatchedFiles: phase1Result.unmatchedFiles.length,
|
|
||||||
errors: phase1Result.errors.length,
|
|
||||||
},
|
|
||||||
phase2: {
|
|
||||||
processedFiles: phase2Result.processedFiles.length,
|
|
||||||
complexMatches: phase2Result.complexMatches,
|
|
||||||
stillUnmatched: phase2Result.stillUnmatched,
|
|
||||||
errors: phase2Result.errors.length,
|
|
||||||
},
|
|
||||||
total: {
|
|
||||||
processed: allMusicFiles.length,
|
|
||||||
matched: phase1Result.quickMatches.length + phase2Result.complexMatches,
|
|
||||||
unmatched: phase2Result.stillUnmatched,
|
|
||||||
errors: phase1Result.errors.length + phase2Result.errors.length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`🎉 Two-Phase Sync completed in ${duration}s:`);
|
|
||||||
console.log(` Phase 1: ${result.phase1.quickMatches} quick matches, ${result.phase1.unmatchedFiles} unmatched`);
|
|
||||||
console.log(` Phase 2: ${result.phase2.complexMatches} complex matches, ${result.phase2.stillUnmatched} still unmatched`);
|
|
||||||
console.log(` Total: ${result.total.processed} processed, ${result.total.matched} matched, ${result.total.unmatched} unmatched`);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export interface Playlist {
|
|
||||||
_id: string;
|
|
||||||
name: string;
|
|
||||||
songs: string[];
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
export interface Song {
|
|
||||||
_id: string;
|
|
||||||
title: string;
|
|
||||||
artist: string;
|
|
||||||
genre: string;
|
|
||||||
bpm: number;
|
|
||||||
key: string;
|
|
||||||
rating: number;
|
|
||||||
comments: string;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user