import React, { useState, useEffect } from 'react'; import { Box, VStack, HStack, Text, Heading, Button, Card, CardBody, CardHeader, Badge, Progress, useToast, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, ModalCloseButton, useDisclosure, SimpleGrid, Stat, StatLabel, StatNumber, StatHelpText, IconButton, Tooltip, Spinner, } from '@chakra-ui/react'; import { FiPlay, FiLink, FiSearch, FiZap, FiMusic, FiCheck, FiX } from 'react-icons/fi'; interface MatchResult { song: any; musicFile: any; confidence: number; matchType: 'exact' | 'fuzzy' | 'partial' | 'none'; matchReason: string; } interface MatchingStats { totalSongs: number; totalMusicFiles: number; matchedMusicFiles: number; unmatchedMusicFiles: number; songsWithoutMusicFiles: number; songsWithMusicFiles: number; matchRate: string; } export const SongMatching: React.FC = () => { const [stats, setStats] = useState(null); const [unmatchedMusicFiles, setUnmatchedMusicFiles] = useState([]); const [matchedMusicFiles, setMatchedMusicFiles] = useState([]); const [songsWithMusicFiles, setSongsWithMusicFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); const [autoLinking, setAutoLinking] = useState(false); const [selectedMusicFile, setSelectedMusicFile] = useState(null); const [suggestions, setSuggestions] = useState([]); const [loadingSuggestions, setLoadingSuggestions] = useState(false); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); useEffect(() => { loadData(); }, []); const loadData = async () => { setIsLoading(true); try { const [statsRes, unmatchedRes, matchedRes, songsWithRes] = await Promise.all([ fetch('/api/matching/stats'), fetch('/api/matching/unmatched-music-files?limit=200'), fetch('/api/matching/matched-music-files?limit=200'), fetch('/api/matching/songs-with-music-files?limit=200') ]); if (statsRes.ok) { const statsData = await statsRes.json(); setStats(statsData.stats); } if (unmatchedRes.ok) { const unmatchedData = await unmatchedRes.json(); setUnmatchedMusicFiles(unmatchedData.musicFiles); } if (matchedRes.ok) { const matchedData = await matchedRes.json(); setMatchedMusicFiles(matchedData.musicFiles); } if (songsWithRes.ok) { const songsData = await songsWithRes.json(); setSongsWithMusicFiles(songsData.songs); } } catch (error) { console.error('Error loading data:', error); toast({ title: 'Error', description: 'Failed to load matching data', status: 'error', duration: 3000, isClosable: true, }); } finally { setIsLoading(false); } }; const handleAutoLink = async () => { setAutoLinking(true); try { const response = await fetch('/api/matching/auto-link', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ minConfidence: 0.7, enableFuzzyMatching: true, enablePartialMatching: false }), }); if (response.ok) { const result = await response.json(); toast({ title: 'Auto-linking Complete', description: `Linked ${result.result.linked} files, ${result.result.unmatched} unmatched`, status: 'success', duration: 5000, isClosable: true, }); loadData(); // Refresh data } else { throw new Error('Auto-linking failed'); } } catch (error) { console.error('Error during auto-linking:', error); toast({ title: 'Error', description: 'Failed to auto-link music files', status: 'error', duration: 3000, isClosable: true, }); } finally { setAutoLinking(false); } }; const handleGetSuggestions = async (musicFile: any) => { setSelectedMusicFile(musicFile); setLoadingSuggestions(true); try { const response = await fetch(`/api/matching/music-file/${musicFile._id}/suggestions`); if (response.ok) { const data = await response.json(); setSuggestions(data.matches); onOpen(); } else { throw new Error('Failed to get suggestions'); } } catch (error) { console.error('Error getting suggestions:', error); toast({ title: 'Error', description: 'Failed to get matching suggestions', status: 'error', duration: 3000, isClosable: true, }); } finally { setLoadingSuggestions(false); } }; const handleLinkMusicFile = async (musicFileId: string, songId: string) => { try { const response = await fetch(`/api/matching/link/${musicFileId}/${songId}`, { method: 'POST', }); if (response.ok) { toast({ title: 'Success', description: 'Music file linked to song successfully', status: 'success', duration: 3000, isClosable: true, }); onClose(); loadData(); // Refresh data } else { throw new Error('Failed to link music file'); } } catch (error) { console.error('Error linking music file:', error); toast({ title: 'Error', description: 'Failed to link music file to song', status: 'error', duration: 3000, isClosable: true, }); } }; const handleUnlinkMusicFile = async (songId: string) => { try { const response = await fetch(`/api/matching/unlink/${songId}`, { method: 'DELETE', }); if (response.ok) { toast({ title: 'Success', description: 'Music file unlinked from song successfully', status: 'success', duration: 3000, isClosable: true, }); loadData(); // Refresh data } else { throw new Error('Failed to unlink music file'); } } catch (error) { console.error('Error unlinking music file:', error); toast({ title: 'Error', description: 'Failed to unlink music file from song', status: 'error', duration: 3000, isClosable: true, }); } }; const getConfidenceColor = (confidence: number) => { if (confidence >= 0.9) return 'green'; if (confidence >= 0.7) return 'blue'; if (confidence >= 0.5) return 'yellow'; return 'red'; }; const canAutoLink = !!stats && stats.unmatchedMusicFiles > 0; if (isLoading) { return ( Loading matching data... ); } return ( {/* Statistics */} {stats && ( Matching Statistics Total Songs {stats.totalSongs} Music Files {stats.totalMusicFiles} Match Rate {stats.matchRate} {stats.matchedMusicFiles} of {stats.totalMusicFiles} files matched Unmatched {stats.unmatchedMusicFiles} {stats.songsWithoutMusicFiles} songs without files )} {/* Auto-Link Button */} Try linking any remaining unmatched files. The main S3 sync already performs matching; use this for leftovers. {/* Unmatched Music Files */} Unmatched Music Files ({unmatchedMusicFiles.length}) {unmatchedMusicFiles.length === 0 ? ( All music files are matched! 🎉 ) : ( {unmatchedMusicFiles.slice(0, 10).map((file) => ( {file.title || file.originalName} {file.artist} {file.album} } size="sm" variant="ghost" colorScheme="blue" onClick={() => handleGetSuggestions(file)} _hover={{ bg: "blue.900" }} /> } size="sm" variant="ghost" colorScheme="green" _hover={{ bg: "green.900" }} /> ))} {unmatchedMusicFiles.length > 10 && ( Showing first 10 of {unmatchedMusicFiles.length} unmatched files )} )} {/* Matched Music Files */} Matched Music Files ({matchedMusicFiles.length}) {matchedMusicFiles.length === 0 ? ( No music files are matched yet. ) : ( {matchedMusicFiles.slice(0, 10).map((file) => ( {file.title || file.originalName} Matched {file.artist} {file.album} } size="sm" variant="ghost" colorScheme="red" onClick={() => handleUnlinkMusicFile(file.songId)} _hover={{ bg: "red.900" }} /> } size="sm" variant="ghost" colorScheme="blue" _hover={{ bg: "blue.900" }} /> ))} {matchedMusicFiles.length > 10 && ( Showing first 10 of {matchedMusicFiles.length} matched files )} )} {/* Songs with Music Files */} Songs with Music Files ({songsWithMusicFiles.length}) {songsWithMusicFiles.length === 0 ? ( No songs have music files linked yet. ) : ( {songsWithMusicFiles.slice(0, 10).map((song) => ( {song.title} Has S3 File {song.artist} {song.location && ( 📁 {song.location} )} {song.s3File?.streamingUrl && ( 🎵 S3: {song.s3File.s3Key} )} } size="sm" variant="ghost" colorScheme="red" onClick={() => handleUnlinkMusicFile(song._id)} _hover={{ bg: "red.900" }} /> } size="sm" variant="ghost" colorScheme="blue" _hover={{ bg: "blue.800" }} /> ))} {songsWithMusicFiles.length > 10 && ( Showing first 10 of {songsWithMusicFiles.length} songs with music files )} )} {/* Suggestions Modal */} Matching Suggestions for "{selectedMusicFile?.title || selectedMusicFile?.originalName}" {loadingSuggestions ? ( Finding matching songs... ) : suggestions.length === 0 ? ( No matching songs found. You can manually link this file later. ) : ( {suggestions.map((suggestion, index) => ( {suggestion.song.title} {Math.round(suggestion.confidence * 100)}% {suggestion.song.artist} {suggestion.song.location && ( 📁 {suggestion.song.location} )} {suggestion.matchReason} } size="sm" variant="ghost" colorScheme="blue" onClick={() => { handleLinkMusicFile(selectedMusicFile._id, suggestion.song._id); onClose(); }} _hover={{ bg: "blue.900" }} /> ))} )} ); };