mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
A single-language interface makes the app less usable for part of the audience and encourages more hardcoded copy as new screens and states are added. Introducing a shared i18n path with a persisted language preference gives the UI one consistent way to present text and lowers the risk of partial or inconsistent localization over time. The surrounding copy cleanup belongs in the same change because it aligns existing labels, messages, and settings text with that translation structure instead of leaving mixed localization approaches in place.
144 lines
4.9 KiB
JavaScript
144 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Testet die Backend-Playback-Pipeline: streamlink → HLS-URL → Manifest-Fetch → Segment-Fetch
|
|
* Tests the backend playback pipeline: streamlink → HLS URL → manifest fetch → segment fetch
|
|
*
|
|
* Usage:
|
|
* node scripts/test-playback-pipeline.mjs twitch.tv/<channel>
|
|
* node scripts/test-playback-pipeline.mjs twitch.tv/videos/<vod-id>
|
|
*/
|
|
|
|
import { spawn } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
const url = process.argv[2]
|
|
if (!url) {
|
|
console.error('Usage: node scripts/test-playback-pipeline.mjs twitch.tv/<channel>')
|
|
process.exit(1)
|
|
}
|
|
|
|
function resolveStreamlinkBin() {
|
|
if (process.platform === 'win32') {
|
|
const candidates = [
|
|
join(process.env.LOCALAPPDATA ?? '', 'Programs', 'Streamlink', 'bin', 'streamlink.exe'),
|
|
join(process.env.ProgramFiles ?? '', 'Streamlink', 'bin', 'streamlink.exe'),
|
|
join(process.env['ProgramFiles(x86)'] ?? '', 'Streamlink', 'bin', 'streamlink.exe'),
|
|
]
|
|
for (const p of candidates) {
|
|
if (existsSync(p)) return p
|
|
}
|
|
}
|
|
return 'streamlink'
|
|
}
|
|
|
|
const STREAMLINK_BIN = resolveStreamlinkBin()
|
|
|
|
function pass(msg) { console.log(` ✓ PASS ${msg}`) }
|
|
function fail(msg) { console.log(` ✗ FAIL ${msg}`) }
|
|
function info(msg) { console.log(` → ${msg}`) }
|
|
|
|
async function run() {
|
|
console.log(`\n=== Playback Pipeline Test ===`)
|
|
console.log(`URL: ${url}`)
|
|
console.log(`Streamlink: ${STREAMLINK_BIN}`)
|
|
console.log(`Streamlink exists: ${existsSync(STREAMLINK_BIN)}`)
|
|
console.log('')
|
|
|
|
// Schritt 1: Streamlink aufrufen
|
|
// Step 1: invoke Streamlink
|
|
console.log('[1] Running Streamlink --stream-url ...')
|
|
const { rawOut, rawErr, code, hlsUrl } = await new Promise((resolve) => {
|
|
const proc = spawn(STREAMLINK_BIN, ['--stream-url', url, 'best'], {
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
let out = ''
|
|
let err = ''
|
|
proc.stdout?.on('data', (d) => { out += d.toString() })
|
|
proc.stderr?.on('data', (d) => { err += d.toString() })
|
|
proc.on('error', (e) => resolve({ rawOut: '', rawErr: String(e), code: -1, hlsUrl: '' }))
|
|
proc.on('exit', (exitCode) => {
|
|
const lines = out.split('\n').map(l => l.trim())
|
|
const hlsUrl = lines.filter(l => l.startsWith('https://')).at(-1) ?? ''
|
|
resolve({ rawOut: out, rawErr: err, code: exitCode, hlsUrl })
|
|
})
|
|
})
|
|
|
|
info(`Exit code: ${code}`)
|
|
info(`stdout (raw):\n${rawOut.trim() || '(empty)'}`)
|
|
if (rawErr.trim()) info(`stderr: ${rawErr.trim().slice(0, 300)}`)
|
|
|
|
if (code !== 0) {
|
|
fail(`streamlink exit code ${code}`)
|
|
process.exit(1)
|
|
}
|
|
if (!hlsUrl) {
|
|
fail('No https:// URL found in stdout')
|
|
process.exit(1)
|
|
}
|
|
pass(`Found HLS URL: ${hlsUrl.slice(0, 80)}...`)
|
|
|
|
// Schritt 2: Manifest fetchen
|
|
// Step 2: fetch the manifest
|
|
console.log('\n[2] Fetching HLS manifest ...')
|
|
let manifestText = ''
|
|
try {
|
|
const res = await fetch(hlsUrl, {
|
|
headers: {
|
|
'Origin': 'https://www.twitch.tv',
|
|
'Referer': 'https://www.twitch.tv/'
|
|
}
|
|
})
|
|
info(`HTTP status: ${res.status}`)
|
|
manifestText = await res.text()
|
|
info(`Manifest (first 200 chars):\n${manifestText.slice(0, 200)}`)
|
|
|
|
if (!manifestText.startsWith('#EXTM3U')) {
|
|
fail('Response is not a valid m3u8 (missing #EXTM3U)')
|
|
process.exit(1)
|
|
}
|
|
pass('#EXTM3U present - valid HLS manifest')
|
|
} catch (e) {
|
|
fail(`Manifest fetch failed: ${e}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Schritt 3: Erstes Segment erreichbar?
|
|
// Step 3: is the first segment reachable?
|
|
console.log('\n[3] Is the first segment reachable? ...')
|
|
const baseUrl = hlsUrl.substring(0, hlsUrl.lastIndexOf('/') + 1)
|
|
const segmentLine = manifestText.split('\n').find(l => l.trim() && !l.startsWith('#'))
|
|
if (!segmentLine) {
|
|
// Möglicherweise Master-Playlist → Sub-Playlist URL suchen
|
|
// Possibly a master playlist -> look for a sub-playlist URL
|
|
const subPlaylistLine = manifestText.split('\n').find(l => l.trim().startsWith('https://'))
|
|
if (subPlaylistLine) {
|
|
info(`Detected master playlist - sub-playlist: ${subPlaylistLine.trim().slice(0, 80)}`)
|
|
pass('Master playlist present (sub-playlists not followed further)')
|
|
} else {
|
|
fail('No segments or sub-playlists found in manifest')
|
|
}
|
|
} else {
|
|
const segmentUrl = segmentLine.startsWith('https://') ? segmentLine.trim() : `${baseUrl}${segmentLine.trim()}`
|
|
info(`Segment URL: ${segmentUrl.slice(0, 80)}`)
|
|
try {
|
|
const segRes = await fetch(segmentUrl, { method: 'HEAD' })
|
|
info(`Segment HTTP status: ${segRes.status}`)
|
|
if (segRes.ok) {
|
|
pass(`First segment is reachable (HTTP ${segRes.status})`)
|
|
} else {
|
|
fail(`First segment returned HTTP ${segRes.status}`)
|
|
}
|
|
} catch (e) {
|
|
fail(`Segment fetch failed: ${e}`)
|
|
}
|
|
}
|
|
|
|
console.log('\n=== Test completed ===\n')
|
|
}
|
|
|
|
run().catch((e) => {
|
|
console.error('Unexpected error:', e)
|
|
process.exit(1)
|
|
})
|