diff --git a/ui/app/components/VideoPlayer.vue b/ui/app/components/VideoPlayer.vue index 27ddaf94..3ff0f4e7 100644 --- a/ui/app/components/VideoPlayer.vue +++ b/ui/app/components/VideoPlayer.vue @@ -429,6 +429,7 @@ let hls: Hls | null = null; const isApple = /(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent); const mediaFile = computed(() => props.item.filename || ''); +const subtitleManifestUrl = computed(() => currentPlaybackUrl('api/player/subtitles/manifest')); const canPlay = computed(() => Boolean(mediaFile.value && !loadingError.value)); const shouldRender = computed(() => active.value && !loading.value); const isPlaying = computed(() => !paused.value); @@ -456,7 +457,7 @@ const { usesAssSubtitleTrack, hasSubtitles, } = usePlayerSubtitles({ - mediaFile, + manifestUrl: subtitleManifestUrl, isVideo: computed(() => !isAudio.value), canPlay, shouldRender, diff --git a/ui/app/composables/usePlayerSubtitles.ts b/ui/app/composables/usePlayerSubtitles.ts index 0aa68d68..355bf24a 100644 --- a/ui/app/composables/usePlayerSubtitles.ts +++ b/ui/app/composables/usePlayerSubtitles.ts @@ -8,7 +8,7 @@ import { toValue, } from 'vue'; import type { SubtitleManifestResponse, SubtitleTrack } from '~/types/subtitles'; -import { encodePath, parse_api_error, request, uri } from '~/utils'; +import { parse_api_error, request } from '~/utils'; type AssRendererInstance = { destroy(): unknown; @@ -22,7 +22,7 @@ type AssRendererConstructor = new ( ) => AssRendererInstance; type UsePlayerSubtitlesOptions = { - mediaFile: MaybeRefOrGetter; + manifestUrl: MaybeRefOrGetter; isVideo: MaybeRefOrGetter; canPlay: MaybeRefOrGetter; shouldRender: MaybeRefOrGetter; @@ -74,7 +74,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) { } async function loadTracks() { - const mediaFile = toValue(options.mediaFile); + const manifestUrl = toValue(options.manifestUrl); const isVideo = toValue(options.isVideo); const canPlay = toValue(options.canPlay); const requestId = ++subtitleRequestId; @@ -84,7 +84,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) { tracks.value = []; subtitleLoadError.value = ''; - if (!mediaFile || !isVideo || !canPlay) { + if (!manifestUrl || !isVideo || !canPlay) { subtitleLoading.value = false; return; } @@ -92,7 +92,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) { subtitleLoading.value = true; try { - const res = await request(uri(`/api/player/subtitles/manifest/${encodePath(mediaFile)}`)); + const res = await request(manifestUrl); const payload = (await res.json()) as SubtitleManifestResponse | { error?: string }; if (!res.ok) { throw new Error(await parse_api_error(payload)); @@ -176,7 +176,7 @@ export function usePlayerSubtitles(options: UsePlayerSubtitlesOptions) { } watch( - () => [toValue(options.mediaFile), toValue(options.isVideo), toValue(options.canPlay)], + () => [toValue(options.manifestUrl), toValue(options.isVideo), toValue(options.canPlay)], () => { void loadTracks(); }, diff --git a/ui/tests/composables/usePlayerSubtitles.test.ts b/ui/tests/composables/usePlayerSubtitles.test.ts index f89e9348..0b22d6bb 100644 --- a/ui/tests/composables/usePlayerSubtitles.test.ts +++ b/ui/tests/composables/usePlayerSubtitles.test.ts @@ -114,7 +114,7 @@ describe('usePlayerSubtitles', () => { ); const { usePlayerSubtitles } = await import('~/composables/usePlayerSubtitles'); - const mediaFile = ref('video file.mkv'); + const manifestUrl = ref('/api/player/subtitles/manifest/video%20file.mkv'); const isVideo = ref(true); const canPlay = ref(true); const shouldRender = ref(false); @@ -123,7 +123,7 @@ describe('usePlayerSubtitles', () => { const { hasSubtitles, nativeSubtitleTrack, selectedSubtitleTrack, usesAssSubtitleTrack } = usePlayerSubtitles({ - mediaFile, + manifestUrl, isVideo, canPlay, shouldRender, @@ -140,6 +140,36 @@ describe('usePlayerSubtitles', () => { expect(usesAssSubtitleTrack.value).toBe(false); }); + it('uses the provided full manifest path including folders', async () => { + fetchMock.mockResolvedValueOnce( + createMockResponse({ + ok: true, + status: 200, + jsonData: { subtitles: [] }, + }), + ); + + const { usePlayerSubtitles } = await import('~/composables/usePlayerSubtitles'); + + usePlayerSubtitles({ + manifestUrl: ref( + '/api/player/subtitles/manifest/youtube/Channel%20Name/Season%202026/video%20file.mkv', + ), + isVideo: ref(true), + canPlay: ref(true), + shouldRender: ref(false), + video: ref(document.createElement('video')), + overlay: ref(document.createElement('div')), + }); + + await flushPromises(); + + expect(fetchMock).toHaveBeenCalledWith( + '/api/player/subtitles/manifest/youtube/Channel%20Name/Season%202026/video%20file.mkv', + expect.anything(), + ); + }); + it('creates and destroys an ASS renderer for ASS subtitles when playback becomes active', async () => { fetchMock.mockResolvedValueOnce( createMockResponse({ @@ -164,7 +194,7 @@ describe('usePlayerSubtitles', () => { const loadRenderer = mock(async () => assConstructorMock as any); const { usePlayerSubtitles } = await import('~/composables/usePlayerSubtitles'); - const mediaFile = ref('video.mkv'); + const manifestUrl = ref('/api/player/subtitles/manifest/video.mkv'); const isVideo = ref(true); const canPlay = ref(true); const shouldRender = ref(false); @@ -172,7 +202,7 @@ describe('usePlayerSubtitles', () => { const overlay = ref(document.createElement('div')); const { usesAssSubtitleTrack } = usePlayerSubtitles({ - mediaFile, + manifestUrl, isVideo, canPlay, shouldRender, @@ -195,7 +225,7 @@ describe('usePlayerSubtitles', () => { expect(assConstructorMock).toHaveBeenCalledTimes(1); expect(assShowMock).toHaveBeenCalledTimes(1); - mediaFile.value = 'second.mkv'; + manifestUrl.value = '/api/player/subtitles/manifest/second.mkv'; fetchMock.mockResolvedValueOnce( createMockResponse({ ok: true, @@ -235,7 +265,7 @@ describe('usePlayerSubtitles', () => { const assLayoutVersion = ref(0); usePlayerSubtitles({ - mediaFile: ref('video.mkv'), + manifestUrl: ref('/api/player/subtitles/manifest/video.mkv'), isVideo: ref(true), canPlay: ref(true), shouldRender: ref(true),