import React, { useEffect, useMemo, useState } from 'react'; import { Box, VStack, Heading, Text, HStack, Badge, Select, Spinner, Table, Thead, Tr, Th, Tbody, Td, IconButton, Tooltip, Switch } from '@chakra-ui/react'; import { FiCheck, FiTrash2 } from 'react-icons/fi'; import { api } from '../services/api'; interface DuplicateItem { songId: string; title: string; artist: string; location?: string; totalTime?: string; averageBpm?: string; bitRate?: string; playlists: string[]; } interface DuplicateGroup { key: string; normalizedTitle: string; normalizedArtist: string; count: number; items: DuplicateItem[]; } export const DuplicatesViewer: React.FC = () => { const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(false); const [minGroupSize, setMinGroupSize] = useState(2); const [processingGroupKey, setProcessingGroupKey] = useState(null); const [onlyWithPlaylists, setOnlyWithPlaylists] = useState(false); const loadDuplicates = async (minSize: number) => { setLoading(true); try { const res = await api.getDuplicateSongs(minSize); setGroups(res.groups || []); } catch (e) { setGroups([]); } finally { setLoading(false); } }; useEffect(() => { loadDuplicates(minGroupSize); // eslint-disable-next-line react-hooks/exhaustive-deps }, [minGroupSize]); const filteredGroups = useMemo(() => { if (!onlyWithPlaylists) return groups; return groups.filter(g => g.items.some(it => (it.playlists || []).length > 0)); }, [groups, onlyWithPlaylists]); return ( Possible Duplicates Minimum group size Only with playlists setOnlyWithPlaylists(e.target.checked)} colorScheme="blue" /> {loading ? ( Scanning duplicates… ) : filteredGroups.length === 0 ? ( No duplicate groups found. ) : ( {filteredGroups.map((group) => ( {group.count} items {group.normalizedArtist} — {group.normalizedTitle} {group.items.map((it) => ( ))}
Keep Title Artist Length BPM Bitrate Playlists Rekordbox Path
} size="sm" colorScheme="green" variant="outline" isLoading={processingGroupKey === group.key} onClick={async () => { setProcessingGroupKey(group.key); try { const merged = Array.from(new Set(group.items.flatMap(x => x.playlists || []))); const allPlaylists = await api.getPlaylists(); const targetId = it.songId; const idsToRemove = new Set(group.items.map(x => x.songId).filter(id => id !== targetId)); const updated = allPlaylists.map(p => { if (p.type === 'playlist') { const tracks = Array.from(new Set([...(p.tracks || []).map(id => idsToRemove.has(id) ? targetId : id)])); if (merged.includes(p.name) && !tracks.includes(targetId)) { tracks.push(targetId); } return { ...p, tracks }; } if (p.type === 'folder' && p.children) { const updateChildren = (nodes: any[]): any[] => nodes.map(node => { if (node.type === 'playlist') { const tracks = Array.from(new Set([...(node.tracks || []).map((id: string) => idsToRemove.has(id) ? targetId : id)])); if (merged.includes(node.name) && !tracks.includes(targetId)) { tracks.push(targetId); } return { ...node, tracks }; } if (node.children) return { ...node, children: updateChildren(node.children) }; return node; }); return { ...p, children: updateChildren(p.children) }; } return p; }); await api.savePlaylists(updated as any); await loadDuplicates(minGroupSize); } finally { setProcessingGroupKey(null); } }} /> } size="sm" colorScheme="red" variant="outline" isLoading={processingGroupKey === group.key} onClick={async () => { setProcessingGroupKey(group.key); try { const targetId = it.songId; const others = group.items.map(x => x.songId).filter(id => id !== targetId); // First merge playlists (safe), then delete redundant songs and optionally their music files const allPlaylists = await api.getPlaylists(); const updated = allPlaylists.map(p => { if (p.type === 'playlist') { const tracks = Array.from(new Set([...(p.tracks || []).map(id => others.includes(id) ? targetId : id)])); return { ...p, tracks }; } if (p.type === 'folder' && p.children) { const updateChildren = (nodes: any[]): any[] => nodes.map(node => { if (node.type === 'playlist') { const tracks = Array.from(new Set([...(node.tracks || []).map((id: string) => others.includes(id) ? targetId : id)])); return { ...node, tracks }; } if (node.children) return { ...node, children: updateChildren(node.children) }; return node; }); return { ...p, children: updateChildren(p.children) }; } return p; }); await api.savePlaylists(updated as any); await api.deleteDuplicateSongs(targetId, others, true); await loadDuplicates(minGroupSize); } finally { setProcessingGroupKey(null); } }} /> {it.title} {it.artist} {it.totalTime || '-'} {it.averageBpm || '-'} {it.bitRate || '-'} {(it.playlists || []).join(', ')} {it.location || '-'}
))}
)}
); };