diff --git a/ui/src/app/app.spec.ts b/ui/src/app/app.spec.ts index 9b4a3a5..5156a3d 100644 --- a/ui/src/app/app.spec.ts +++ b/ui/src/app/app.spec.ts @@ -413,4 +413,64 @@ describe('App', () => { expect(errorSpy).toHaveBeenCalledWith('Subscription name must not be empty'); errorSpy.mockRestore(); }); + // Issue #533: the server picks AUDIO_DOWNLOAD_DIR on download_type alone + // (ytdl.py), so the UI's choice of URL base has to use the same rule. It used + // to also treat any .mp3 as audio, which pointed the link at audio_download/ + // for files the server had written to DOWNLOAD_DIR. + describe('download links follow the server directory rule (#533)', () => { + const makeDownload = (over: Partial): Download => ({ + id: 'vid1', + title: 'Test', + url: 'https://example.com/v', + download_type: 'video', + quality: 'best', + format: 'any', + folder: '', + custom_name_prefix: '', + playlist_item_limit: 0, + status: 'finished', + msg: '', + percent: 100, + speed: 0, + eta: 0, + filename: 'song.mp4', + checked: false, + ...over, + } as Download); + + const appWithDirs = () => { + const fixture = TestBed.createComponent(App); + const app = fixture.componentInstance; + const downloads = TestBed.inject(DownloadsService) as unknown as DownloadsServiceStub; + downloads.configuration['PUBLIC_HOST_URL'] = 'download/'; + downloads.configuration['PUBLIC_HOST_AUDIO_URL'] = 'audio_download/'; + return app; + }; + + it('uses the audio base for an audio download', () => { + const app = appWithDirs(); + const link = app.buildDownloadLink(makeDownload({ download_type: 'audio', filename: 'song.mp3' })); + expect(link).toBe('audio_download/song.mp3'); + }); + + it('uses the video base for an mp3 produced by a video download', () => { + const app = appWithDirs(); + const link = app.buildDownloadLink(makeDownload({ download_type: 'video', filename: 'song.mp3' })); + expect(link).toBe('download/song.mp3'); + }); + + it('uses the video base for a video download', () => { + const app = appWithDirs(); + const link = app.buildDownloadLink(makeDownload({ filename: 'clip.mp4' })); + expect(link).toBe('download/clip.mp4'); + }); + + it('applies the same rule to chapter links', () => { + const app = appWithDirs(); + const dl = makeDownload({ download_type: 'video' }); + expect(app.buildChapterDownloadLink(dl, 'ch1.mp3')).toBe('download/ch1.mp3'); + const audio = makeDownload({ download_type: 'audio' }); + expect(app.buildChapterDownloadLink(audio, 'ch1.mp3')).toBe('audio_download/ch1.mp3'); + }); + }); }); diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 81b51cc..4aac535 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -1284,7 +1284,12 @@ export class App implements AfterViewInit, OnInit, OnDestroy { buildDownloadLink(download: Download) { let baseDir = this.downloads.configuration["PUBLIC_HOST_URL"]; - if (download.download_type === 'audio' || download.filename.endsWith('.mp3')) { + // Must match the server's directory rule exactly: ytdl.py writes to + // AUDIO_DOWNLOAD_DIR on download_type alone. Treating any .mp3 as audio + // sent the link to audio_download/ for mp3s produced under a video-type + // download (a postprocessor, a preset, or a legacy record), which the + // server had written to DOWNLOAD_DIR -- a 404 whenever the two differ. + if (download.download_type === 'audio') { baseDir = this.downloads.configuration["PUBLIC_HOST_AUDIO_URL"]; } @@ -1382,7 +1387,8 @@ export class App implements AfterViewInit, OnInit, OnDestroy { buildChapterDownloadLink(download: Download, chapterFilename: string) { let baseDir = this.downloads.configuration["PUBLIC_HOST_URL"]; - if (download.download_type === 'audio' || chapterFilename.endsWith('.mp3')) { + // Same server-side rule as buildDownloadLink above. + if (download.download_type === 'audio') { baseDir = this.downloads.configuration["PUBLIC_HOST_AUDIO_URL"]; }