Skip to content

Commit 7b7f689

Browse files
committed
Merge remote-tracking branch 'origin/dev' into feature/traktor-nml-export
2 parents 69a6cee + 5d1a2c7 commit 7b7f689

7 files changed

Lines changed: 114 additions & 7 deletions

File tree

renderer/src/Sidebar.jsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ function Sidebar({
9898

9999
setImportProgress({ total: files.length, completed: 0 });
100100
await window.api.importAudioFiles(files, playlistId);
101-
setImportProgress({ total: 0, completed: 0 });
101+
// Small delay so the user sees 100% before the bar disappears
102+
setTimeout(() => setImportProgress({ total: 0, completed: 0 }), 800);
102103
};
103104

104105
const handleCreatePlaylist = async (e) => {
@@ -137,6 +138,13 @@ function Sidebar({
137138
return unsub;
138139
}, []);
139140

141+
useEffect(() => {
142+
const unsub = window.api.onImportProgress(({ completed, total }) => {
143+
setImportProgress({ completed, total });
144+
});
145+
return unsub;
146+
}, []);
147+
140148
useEffect(() => {
141149
const unsub = window.api.onNormalizeProgress((data) => {
142150
if (data.done) {

renderer/src/__tests__/setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ window.api = {
4646
onDepsProgress: vi.fn().mockImplementation(noop),
4747
onMoveLibraryProgress: vi.fn().mockImplementation(noop),
4848
onExportM3UProgress: vi.fn().mockImplementation(noop),
49+
onImportProgress: vi.fn().mockImplementation(noop),
4950
onNormalizeProgress: vi.fn().mockImplementation(noop),
5051
getMediaPort: vi.fn().mockResolvedValue(19876),
5152
ytDlpFetchInfo: vi.fn().mockResolvedValue({ ok: false, error: 'not configured' }),

src/__tests__/importManager.test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,77 @@ describe('normalizeAudioFile', () => {
236236
expect(args[iIndex + 1]).toBe(TRACK.file_path);
237237
});
238238
});
239+
240+
// ── Artist detection from filename ────────────────────────────────────────────
241+
242+
import { ffprobe } from '../audio/ffmpeg.js';
243+
244+
describe('importAudioFile — artist detection from filename', () => {
245+
it('uses ID3 artist tag when present, ignoring filename', async () => {
246+
ffprobe.mockResolvedValueOnce({
247+
format: {
248+
format_name: 'mp3',
249+
duration: '180.0',
250+
bit_rate: '320000',
251+
tags: { title: 'My Song', artist: 'Tag Artist' },
252+
},
253+
streams: [],
254+
});
255+
256+
await importAudioFile('/music/Someone Else - My Song.mp3');
257+
258+
expect(mockAddTrack.mock.calls[0][0].artist).toBe('Tag Artist');
259+
});
260+
261+
it('parses artist from "Artist - Title" filename when artist tag is missing', async () => {
262+
ffprobe.mockResolvedValueOnce({
263+
format: {
264+
format_name: 'mp3',
265+
duration: '180.0',
266+
bit_rate: '320000',
267+
tags: { title: '', artist: '' },
268+
},
269+
streams: [],
270+
});
271+
272+
await importAudioFile('/music/Deadmau5 - Some Chords.mp3');
273+
274+
expect(mockAddTrack.mock.calls[0][0].artist).toBe('Deadmau5');
275+
expect(mockAddTrack.mock.calls[0][0].title).toBe('Some Chords');
276+
});
277+
278+
it('leaves artist empty when no tag and no dash in filename', async () => {
279+
ffprobe.mockResolvedValueOnce({
280+
format: {
281+
format_name: 'mp3',
282+
duration: '180.0',
283+
bit_rate: '320000',
284+
tags: { title: '', artist: '' },
285+
},
286+
streams: [],
287+
});
288+
289+
await importAudioFile('/music/untitled_track.mp3');
290+
291+
expect(mockAddTrack.mock.calls[0][0].artist).toBe('');
292+
expect(mockAddTrack.mock.calls[0][0].title).toBe('untitled_track');
293+
});
294+
295+
it('keeps ID3 title when artist is missing but filename has dash', async () => {
296+
ffprobe.mockResolvedValueOnce({
297+
format: {
298+
format_name: 'mp3',
299+
duration: '180.0',
300+
bit_rate: '320000',
301+
tags: { title: 'ID3 Title', artist: '' },
302+
},
303+
streams: [],
304+
});
305+
306+
await importAudioFile('/music/Filename Artist - Other Title.mp3');
307+
308+
expect(mockAddTrack.mock.calls[0][0].artist).toBe('Filename Artist');
309+
// ID3 title wins over filename-derived title
310+
expect(mockAddTrack.mock.calls[0][0].title).toBe('ID3 Title');
311+
});
312+
});

src/audio/importManager.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,24 @@ export async function importAudioFile(filePath, sourceMeta = {}) {
221221
// Extract tags
222222
const { title, artist, album, genre, year, label, bpm } = parseTags(probe);
223223

224+
// Fallback: parse "Artist - Title" from filename when artist tag is absent
225+
const basename = path.basename(filePath, ext);
226+
let resolvedArtist = artist;
227+
let resolvedTitle = title;
228+
if (!artist) {
229+
const dashIdx = basename.indexOf(' - ');
230+
if (dashIdx !== -1) {
231+
resolvedArtist = basename.slice(0, dashIdx).trim();
232+
resolvedTitle = resolvedTitle || basename.slice(dashIdx + 3).trim();
233+
}
234+
}
235+
224236
// Extract embedded album art (best-effort, non-blocking)
225237
const artworkPath = await extractArtwork(dest, hash);
226238

227239
const trackId = addTrack({
228-
title: title || path.basename(filePath, ext),
229-
artist,
240+
title: resolvedTitle || basename,
241+
artist: resolvedArtist,
230242
album,
231243
duration,
232244
file_path: dest,
@@ -245,7 +257,7 @@ export async function importAudioFile(filePath, sourceMeta = {}) {
245257
artwork_path: artworkPath ?? null,
246258
});
247259

248-
console.log(`Added track ID ${trackId}: ${title || path.basename(filePath, ext)}`);
260+
console.log(`Added track ID ${trackId}: ${resolvedTitle || basename}`);
249261

250262
spawnAnalysis(trackId, dest);
251263
return trackId;

src/deps.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,19 @@ export function getReleaseByTag(owner, repo, tag) {
164164
// ── Archive helpers ───────────────────────────────────────────────────────────
165165

166166
async function extractTarGz(archive, destDir) {
167+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
167168
await fs.promises.mkdir(destDir, { recursive: true });
168169
await execAsync(`tar -xzf "${archive}" -C "${destDir}"`);
169170
}
170171

171172
async function extractTarXz(archive, destDir) {
173+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
172174
await fs.promises.mkdir(destDir, { recursive: true });
173175
await execAsync(`tar -xJf "${archive}" -C "${destDir}"`);
174176
}
175177

176178
async function extractZip(archive, destDir) {
179+
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
177180
await fs.promises.mkdir(destDir, { recursive: true });
178181
if (process.platform === 'win32') {
179182
await execAsync(

src/main.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,13 +535,17 @@ ipcMain.handle('open-dir-dialog', async () => {
535535
ipcMain.handle('import-audio-files', async (event, filePaths, playlistId) => {
536536
console.log('Importing audio files:', filePaths);
537537
const trackIds = [];
538+
const total = filePaths.length;
538539

539-
for (const filePath of filePaths) {
540+
for (let i = 0; i < total; i++) {
540541
try {
541-
const trackId = await importAudioFile(filePath);
542+
const trackId = await importAudioFile(filePaths[i]);
542543
trackIds.push(trackId);
543544
} catch (err) {
544-
console.error('Import failed:', filePath, err);
545+
console.error('Import failed:', filePaths[i], err);
546+
}
547+
if (global.mainWindow) {
548+
global.mainWindow.webContents.send('import-progress', { completed: i + 1, total });
545549
}
546550
}
547551

src/preload.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ contextBridge.exposeInMainWorld('api', {
8787
ipcRenderer.on('library-updated', handler);
8888
return () => ipcRenderer.removeListener('library-updated', handler);
8989
},
90+
onImportProgress: (callback) => {
91+
const handler = (_, data) => callback(data);
92+
ipcRenderer.on('import-progress', handler);
93+
return () => ipcRenderer.removeListener('import-progress', handler);
94+
},
9095
onPlaylistsUpdated: (callback) => {
9196
const handler = () => callback();
9297
ipcRenderer.on('playlists-updated', handler);

0 commit comments

Comments
 (0)