From fc90617e9ca12750ccd74c38a2522864fe430e56 Mon Sep 17 00:00:00 2001 From: kveld9 Date: Wed, 15 Apr 2026 11:50:44 -0300 Subject: [PATCH] some fixes --- src/locales/de.ts | 3 ++ src/locales/en.ts | 3 ++ src/locales/es.ts | 3 ++ src/locales/fr.ts | 3 ++ src/locales/nb.ts | 3 ++ src/locales/nl.ts | 3 ++ src/locales/ru.ts | 3 ++ src/locales/zh.ts | 16 ++++++++++ src/pages/Artists.tsx | 2 +- src/pages/PlaylistDetail.tsx | 60 ++++++++++++++++++------------------ 10 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/locales/de.ts b/src/locales/de.ts index d66ef415..8b0df928 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -999,6 +999,9 @@ export const deTranslation = { csvImportNetworkErrorsTitle: 'Netzwerkfehler (Import kann wiederholt werden):', csvImportDownloadReport: 'Bericht herunterladen', csvImportClose: 'Schließen', + csvImportNoValidTracks: 'Keine gültigen Titel in CSV-Datei gefunden', + csvImportFailed: 'CSV-Import fehlgeschlagen', + csvImportToast: '{{added}} hinzugefügt, {{notFound}} nicht gefunden, {{duplicates}} Duplikate', }, mostPlayed: { title: 'Meistgehört', diff --git a/src/locales/en.ts b/src/locales/en.ts index 5ed3e481..c49b7d7c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1001,6 +1001,9 @@ export const enTranslation = { csvImportNetworkErrorsTitle: 'Network Errors (may retry import):', csvImportDownloadReport: 'Download Report', csvImportClose: 'Close', + csvImportNoValidTracks: 'No valid tracks found in CSV file', + csvImportFailed: 'Failed to import CSV file', + csvImportToast: '{{added}} added, {{notFound}} not found, {{duplicates}} duplicates', }, mostPlayed: { title: 'Most Played', diff --git a/src/locales/es.ts b/src/locales/es.ts index ff51c9f9..16a534e3 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1002,6 +1002,9 @@ export const esTranslation = { csvImportNetworkErrorsTitle: 'Errores de Red (puede reintentar la importación):', csvImportDownloadReport: 'Descargar Reporte', csvImportClose: 'Cerrar', + csvImportNoValidTracks: 'No se encontraron canciones válidas en el archivo CSV', + csvImportFailed: 'Error al importar el archivo CSV', + csvImportToast: '{{added}} agregadas, {{notFound}} no encontradas, {{duplicates}} duplicadas', }, mostPlayed: { title: 'Más Reproducidos', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 45d358d6..4c4a252f 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -997,6 +997,9 @@ export const frTranslation = { csvImportNetworkErrorsTitle: 'Erreurs Réseau (peut réessayer l\'import):', csvImportDownloadReport: 'Télécharger le Rapport', csvImportClose: 'Fermer', + csvImportNoValidTracks: 'Aucun titre valide trouvé dans le fichier CSV', + csvImportFailed: 'Échec de l\'import CSV', + csvImportToast: '{{added}} ajoutés, {{notFound}} non trouvés, {{duplicates}} doublons', }, mostPlayed: { title: 'Les plus joués', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 7c41fffc..da5afbde 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -996,6 +996,9 @@ export const nbTranslation = { csvImportNetworkErrorsTitle: 'Nettverksfeil (kan prøve import på nytt):', csvImportDownloadReport: 'Last ned rapport', csvImportClose: 'Lukk', + csvImportNoValidTracks: 'Ingen gyldige spor funnet i CSV-filen', + csvImportFailed: 'Kunne ikke importere CSV-fil', + csvImportToast: '{{added}} lagt til, {{notFound}} ikke funnet, {{duplicates}} duplikater', }, mostPlayed: { title: 'Mest spilt', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 6745d453..3bec19c8 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -996,6 +996,9 @@ export const nlTranslation = { csvImportNetworkErrorsTitle: 'Netwerkfouten (import kan opnieuw proberen):', csvImportDownloadReport: 'Rapport downloaden', csvImportClose: 'Sluiten', + csvImportNoValidTracks: 'Geen geldige nummers gevonden in CSV-bestand', + csvImportFailed: 'CSV-import mislukt', + csvImportToast: '{{added}} toegevoegd, {{notFound}} niet gevonden, {{duplicates}} duplicaten', }, mostPlayed: { title: 'Meest gespeeld', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 18808ce5..0854bd50 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1055,6 +1055,9 @@ export const ruTranslation = { csvImportNetworkErrorsTitle: 'Ошибки сети (можно повторить импорт):', csvImportDownloadReport: 'Скачать отчёт', csvImportClose: 'Закрыть', + csvImportNoValidTracks: 'В CSV-файле не найдено действительных треков', + csvImportFailed: 'Не удалось импортировать CSV-файл', + csvImportToast: '{{added}} добавлено, {{notFound}} не найдено, {{duplicates}} дубликатов', }, mostPlayed: { title: 'Популярное', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 9044107b..2138fc8e 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -977,6 +977,22 @@ export const zhTranslation = { addToPlaylistSuccess: '{{count}} 首歌曲已添加到 {{playlist}}', addToPlaylistNoNew: '{{playlist}} 没有新歌曲', addToPlaylistError: '添加到播放列表时出错', + importCSV: '导入 CSV', + importCSVTooltip: '从 Spotify CSV 导入', + csvImportReport: 'CSV 导入报告', + csvImportTotal: '总计', + csvImportAdded: '已添加', + csvImportDuplicates: '重复项', + csvImportNotFound: '未找到', + csvImportNetworkErrors: '网络错误', + csvImportDuplicatesTitle: '重复曲目(已在播放列表中):', + csvImportNotFoundTitle: '未找到的曲目:', + csvImportNetworkErrorsTitle: '网络错误(可重试导入):', + csvImportDownloadReport: '下载报告', + csvImportClose: '关闭', + csvImportNoValidTracks: 'CSV 文件中未找到有效曲目', + csvImportFailed: 'CSV 文件导入失败', + csvImportToast: '{{added}} 已添加,{{notFound}} 未找到,{{duplicates}} 重复项', }, mostPlayed: { title: '最常播放', diff --git a/src/pages/Artists.tsx b/src/pages/Artists.tsx index d7f1ce38..64726816 100644 --- a/src/pages/Artists.tsx +++ b/src/pages/Artists.tsx @@ -82,7 +82,7 @@ export default function Artists() { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const showArtistImages = useAuthStore(s => s.showArtistImages); - const PAGE_SIZE = showArtistImages ? 50 : 100; // Menor con imágenes para reducir I/O + const PAGE_SIZE = showArtistImages ? 50 : 100; // Smaller with images to reduce I/O const [visibleCount, setVisibleCount] = useState(PAGE_SIZE); const [loadingMore, setLoadingMore] = useState(false); const observerTarget = useRef(null); diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index c30eb269..569c7bde 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -61,12 +61,12 @@ function codecLabel(song: SubsonicSong, showBitrate: boolean): string { interface SpotifyCsvTrack { trackName: string; artistName: string; - artistNames: string[]; // Array de todos los artistas para mejor matching + artistNames: string[]; // Array of all artists for better matching albumName: string; isrc?: string; } -// Mapeo de headers a campos canónicos (soporta inglés y español) +// Header mapping to canonical fields (supports English and Spanish) const HEADER_MAPPINGS: Record = { // Track name 'track name': 'trackName', @@ -145,7 +145,7 @@ function extractFeaturedArtists(title: string): string[] { } function parseSpotifyCsv(csvContent: string): SpotifyCsvTrack[] { - // Strip BOM y parsear con Papa Parse + // Strip BOM and parse with Papa Parse const cleanContent = csvContent.replace(/^\uFEFF/, ''); const parseResult = Papa.parse(cleanContent, { @@ -163,7 +163,7 @@ function parseSpotifyCsv(csvContent: string): SpotifyCsvTrack[] { const data = parseResult.data as Record[]; - // Verificar columnas requeridas + // Verify required columns if (!data.length || !data[0].trackName || !data[0].artistName) { console.error('CSV columns not found. Available headers:', Object.keys(data[0] || {})); return []; @@ -181,7 +181,7 @@ function parseSpotifyCsv(csvContent: string): SpotifyCsvTrack[] { if (!trackName || !artistField) continue; - // Parsear múltiples artistas del campo + extraer colaboradores del título + // Parse multiple artists from field + extract collaborators from title const artistNames = parseArtists(artistField); const featuredArtists = extractFeaturedArtists(trackName); const allArtists = [...new Set([...artistNames, ...featuredArtists])]; @@ -508,7 +508,7 @@ export default function PlaylistDetail() { }; // ── CSV Import ────────────────────────────────────────────── - // Normalizar strings para matching: quita tildes, caracteres especiales, lowercase, trim + // Normalize strings for matching: remove accents, special chars, lowercase, trim const normalizeForMatching = (s: string): string => s.toLowerCase() .normalize('NFD') @@ -517,7 +517,7 @@ export default function PlaylistDetail() { .replace(/[æ]/gi, 'ae') .trim(); - // Limpia sufijos comunes de títulos (remasterizados, en vivo, ediciones, etc.) + // Clean common title suffixes (remastered, live, editions, etc.) const cleanTrackTitle = (title: string): string => { const suffixes = [ // Remastered variants @@ -612,19 +612,19 @@ export default function PlaylistDetail() { /\s*\(\d{4}\)$/i, ]; let cleaned = title.trim(); - // Aplicar patrones múltiples veces para casos anidados + // Apply patterns multiple times for nested cases for (let i = 0; i < 3; i++) { const previous = cleaned; for (const regex of suffixes) { cleaned = cleaned.replace(regex, ''); } cleaned = cleaned.trim(); - if (previous === cleaned) break; // No más cambios + if (previous === cleaned) break; // No more changes } return cleaned; }; - // Distancia de Levenshtein para scoring de similitud + // Levenshtein distance for similarity scoring const levenshtein = (a: string, b: string): number => { const matrix: number[][] = []; for (let i = 0; i <= b.length; i++) matrix[i] = [i]; @@ -642,12 +642,12 @@ export default function PlaylistDetail() { const similarityScore = (a: string, b: string): number => { const maxLen = Math.max(a.length, b.length); if (maxLen === 0) return 1; - // Usar strings normalizados (sin tildes) para comparación + // Use normalized strings (without accents) for comparison const dist = levenshtein(normalizeForMatching(a), normalizeForMatching(b)); return 1 - dist / maxLen; }; - // Procesa búsquedas en batches para no saturar el servidor + // Process searches in batches to avoid overloading the server const processBatch = async ( items: T[], batchSize: number, @@ -679,7 +679,7 @@ export default function PlaylistDetail() { const csvTracks = parseSpotifyCsv(content); if (csvTracks.length === 0) { - showToast('No valid tracks found in CSV file', 3000, 'error'); + showToast(t('playlists.csvImportNoValidTracks'), 3000, 'error'); setCsvImporting(false); return; } @@ -691,10 +691,10 @@ export default function PlaylistDetail() { const duplicateTracks: SpotifyCsvTrack[] = []; let duplicateCount = 0; - // Procesar en batches de 10 para balance velocidad/servidor + // Process in batches of 10 to balance speed/server load await processBatch(csvTracks, 10, async (track) => { try { - // Retry: 2 intentos en caso de error de red + // Retry: 2 attempts in case of network error let searchResult; let attempts = 0; const maxAttempts = 2; @@ -706,7 +706,7 @@ export default function PlaylistDetail() { } catch (err) { attempts++; if (attempts >= maxAttempts) throw err; - // Esperar 500ms antes de reintentar + // Wait 500ms before retrying await new Promise(r => setTimeout(r, 500)); } } @@ -716,32 +716,32 @@ export default function PlaylistDetail() { return null; } - // Scoring de confianza para cada resultado - // Limpiar título del CSV para comparación justa + // Confidence scoring for each result + // Clean CSV title for fair comparison const cleanCsvTitle = cleanTrackTitle(track.trackName); const scoredMatches = searchResult.songs.map(s => { - // Vía rápida ISRC: si ambos tienen ISRC y coinciden, score perfecto + // Fast ISRC path: if both have ISRC and they match, perfect score if (track.isrc && s.isrc && track.isrc.toUpperCase() === s.isrc.toUpperCase()) { return { song: s, score: 1.0, titleScore: 1.0, artistScore: 1.0, isrcMatch: true }; } - // Limpiar título del resultado también + // Clean the result title as well const cleanResultTitle = cleanTrackTitle(s.title); const titleScore = similarityScore(cleanResultTitle, cleanCsvTitle); - // Scoring de artista: máximo score contra cualquiera de los artistas del CSV + // Artist scoring: maximum score against any of the CSV artists const artistScore = s.artist ? Math.max(...track.artistNames.map(csvArtist => similarityScore(s.artist || '', csvArtist) )) : 0; - // Si no hay álbum en CSV o en local, usar 1.0 (neutral) para no penalizar + // If no album in CSV or local, use 1.0 (neutral) to avoid penalizing const albumScore = (s.album && track.albumName) ? similarityScore(s.album, track.albumName) : 1.0; - // Peso dinámico: títulos específicos (>4 palabras) → más peso al título + // Dynamic weight: specific titles (>4 words) → more weight to title const titleWords = cleanCsvTitle.split(/\s+/).length; const isSpecificTitle = titleWords > 4; const titleWeight = isSpecificTitle ? 0.55 : 0.4; @@ -809,10 +809,10 @@ export default function PlaylistDetail() { ? ` (${searchErrors.length} network errors - may retry)` : ''; - // Determinar tipo de toast según resultados: - // - success: todas las canciones fueron añadidas correctamente - // - warning: al menos una añadida, pero algunas no se encontraron/duplicadas - // - error: ninguna añadida (todas duplicadas o no encontradas) + // Determine toast type based on results: + // - success: all songs were added successfully + // - warning: at least one added, but some not found/duplicates + // - error: none added (all duplicates or not found) const hasAdded = addedSongs.length > 0; const hasIssues = notFound.length > 0 || duplicateCount > 0 || searchErrors.length > 0; @@ -826,13 +826,13 @@ export default function PlaylistDetail() { } showToast( - `${addedSongs.length} added, ${notFound.length} not found, ${duplicateCount} duplicates${errorMsg}`, + t('playlists.csvImportToast', { added: addedSongs.length, notFound: notFound.length, duplicates: duplicateCount }) + errorMsg, 5000, toastVariant ); } catch (err) { console.error('CSV import failed:', err); - showToast('Failed to import CSV file', 3000, 'error'); + showToast(t('playlists.csvImportFailed'), 3000, 'error'); } finally { setCsvImporting(false); } @@ -1999,7 +1999,7 @@ function CsvImportReportModal({ report, playlistName, onClose }: CsvReportModalP )} -
+