fix(aiui): play the node's own files before reaching for the network
usePlayer.play() never looked at song.sources[] — a real library track went straight to (CSP-blocked) Wavlake and reported 'Not found on Wavlake' while its bytes sat on the operator's disk. Node sources (same-origin /content/<id>, Range-streamed) now play first; Wavlake is the metadata-only fallback. FilmDetail likewise only played YouTube sources; own/peer/IndeeHub sources (same-origin, media-src 'self') now win, YouTube stays the free-films fallback. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -131,8 +131,15 @@ const videoStore = useVideoPlayerStore()
|
||||
|
||||
const isExternal = computed(() => props.film.id.startsWith('ext-'))
|
||||
|
||||
// Node files first: own/peer/IndeeHub sources are same-origin `/content/<id>`
|
||||
// streams the node serves with Range support (media-src 'self' allows them).
|
||||
// YouTube is the free-films fallback, not the default — a real library file
|
||||
// must never route out to YouTube when its bytes are on the operator's node.
|
||||
const NODE_SOURCE_TYPES = new Set(['nextcloud', 'plex', 'indeehub'])
|
||||
const playableSource = computed(() =>
|
||||
props.film.sources.find(s => s.type === 'youtube' || s.url.includes('youtube.com'))
|
||||
props.film.sources.find(
|
||||
s => NODE_SOURCE_TYPES.has(s.type) && (s.url.startsWith('/') || s.url.startsWith(window.location.origin)),
|
||||
) ?? props.film.sources.find(s => s.type === 'youtube' || s.url.includes('youtube.com'))
|
||||
)
|
||||
|
||||
function openVideo() {
|
||||
|
||||
@@ -38,8 +38,7 @@ describe('usePlayer', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('returns expected API shape', () => {
|
||||
const player = usePlayer()
|
||||
it('returns expected API shape', () => { const player = usePlayer()
|
||||
expect(player.currentSong).toBeDefined()
|
||||
expect(player.isPlaying).toBeDefined()
|
||||
expect(player.isLoading).toBeDefined()
|
||||
@@ -54,6 +53,23 @@ describe('usePlayer', () => {
|
||||
expect(player.clearQueue).toBeTypeOf('function')
|
||||
})
|
||||
|
||||
it('plays a node-source song directly without touching Wavlake', async () => {
|
||||
const fetchSpy = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchSpy)
|
||||
const player = usePlayer()
|
||||
const song = {
|
||||
...makeSong('n1', 'Node Track', 'Node Artist'),
|
||||
sources: [{ type: 'funkwhale', name: 'This node', url: '/content/abc-123' }],
|
||||
}
|
||||
await player.play(song as never)
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
expect(player.isLoading.value).toBe(false)
|
||||
expect(player.error.value).toBeNull()
|
||||
expect(player.currentSong.value?.id).toBe('n1')
|
||||
expect(player.playableSource.value?.url).toBe('/content/abc-123')
|
||||
expect(player.playableSource.value?.source).toBe('node')
|
||||
})
|
||||
|
||||
it('starts with no track', () => {
|
||||
const player = usePlayer()
|
||||
expect(player.hasTrack.value).toBe(false)
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'plyr/dist/plyr.css'
|
||||
import { apiFetch } from '@/utils/api-fetch'
|
||||
|
||||
interface MusicSearchResult {
|
||||
source: 'wavlake'
|
||||
source: 'wavlake' | 'node'
|
||||
type: 'stream'
|
||||
url: string
|
||||
title?: string
|
||||
@@ -250,6 +250,32 @@ export function usePlayer() {
|
||||
currentTime.value = 0
|
||||
duration.value = 0
|
||||
|
||||
// A real node track carries its own sources (same-origin `/content/<id>`,
|
||||
// Range-streamed by the node itself) — play those FIRST. Wavlake is the
|
||||
// metadata-only fallback; on a node it is CSP-blocked outright, so every
|
||||
// library track used to end at "Not found on Wavlake" without ever
|
||||
// trying the bytes sitting on the operator's own disk.
|
||||
const nodeSource = song.sources?.find(
|
||||
(s) => !!s.url && (s.url.startsWith('/') || s.url.startsWith(window.location.origin)),
|
||||
)
|
||||
if (nodeSource) {
|
||||
isLoading.value = false
|
||||
error.value = null
|
||||
playableSource.value = {
|
||||
source: 'node',
|
||||
type: 'stream',
|
||||
url: nodeSource.url,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
coverUrl: song.coverUrl,
|
||||
duration: song.duration,
|
||||
}
|
||||
if (containerEl) {
|
||||
initPlayer(playableSource.value)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const query = `${song.title} ${song.artist}`.trim()
|
||||
const result = await searchMusic(query, song.title, song.artist)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user