Movies & TV: browse and play your Plex movies and shows (resume, progress to Plex, next episode, quality, audio and subtitles, direct play or HLS conversion)
build-container / build (push) Successful in 20m38s
build-container / build (push) Successful in 20m38s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
84a0ea10e9
commit
f3216b2f3b
11 files changed
+300
-9
No files matched your search
@@ -45,6 +45,8 @@ const jsonOut = (rs, code, o) => { rs.writeHead(code, { 'content-type': 'applica
|
||||
const makePlex = () => listen((rq, rs) => {
|
||||
if (rq.url === '/identity') return jsonOut(rs, 200, { MediaContainer: { machineIdentifier: 'sid1' } });
|
||||
plexReqs.push(rq.method + ' ' + rq.url);
|
||||
if (rq.url.startsWith('/video/:/transcode/universal/start.m3u8')) { rs.writeHead(200, { 'content-type': 'application/vnd.apple.mpegurl' }); return rs.end('#EXTM3U\n#EXT-X-MAP:URI="/video/:/transcode/universal/session/S/init.mp4"\n#EXTINF:4,\nsession/S/seg0.ts\n#EXTINF:4,\n/video/:/transcode/universal/session/S/seg1.ts\n#EXT-X-ENDLIST\n'); }
|
||||
if (rq.url.startsWith('/library/parts/1/2/file.mp4')) { const rg = rq.headers.range; if (rg) { rs.writeHead(206, { 'content-type': 'video/mp4', 'content-range': 'bytes 0-9/100', 'accept-ranges': 'bytes', 'content-length': '10' }); return rs.end('0123456789'); } rs.writeHead(200, { 'content-type': 'video/mp4', 'accept-ranges': 'bytes' }); return rs.end('x'.repeat(100)); }
|
||||
seen.push(rq.headers['x-plex-token']);
|
||||
if (![tv.serverToken, 'KIDSRV', 'GUESTSRV'].includes(rq.headers['x-plex-token'])) { rs.writeHead(401); return rs.end(); }
|
||||
jsonOut(rs, 200, { MediaContainer: { Directory: [{ key: '1', type: 'artist', title: 'Music' }] } });
|
||||
@@ -116,6 +118,13 @@ const jsonOut = (rs, code, o) => { rs.writeHead(code, { 'content-type': 'applica
|
||||
r = await call('/plex/library/sections'); ok('AFTER A RESTART: their Plex pages load', r.status === 200, r.status);
|
||||
await call('/api/plex/switch', { id: '1' });
|
||||
|
||||
// ================= Movies & TV: the pass-through carries video =================
|
||||
r = await fetch(base + '/plex/library/parts/1/2/file.mp4', { headers: { cookie, range: 'bytes=0-9' } }); const rb = await r.text();
|
||||
ok('a video file is streamed with Range, so the browser can seek (206 and the range come back)', r.status === 206 && rb === '0123456789' && r.headers.get('content-range') === 'bytes 0-9/100' && r.headers.get('accept-ranges') === 'bytes', [r.status, r.headers.get('content-range')]);
|
||||
r = await fetch(base + '/plex/video/:/transcode/universal/start.m3u8?path=%2Flibrary%2Fmetadata%2F5&protocol=hls', { headers: { cookie } }); const pl = await r.text();
|
||||
ok('a converted-video playlist keeps its addresses under /plex (relative ones untouched, absolute ones prefixed)', r.status === 200 && /mpegurl/.test(r.headers.get('content-type')) && pl.includes('URI="/plex/video/:/transcode/universal/session/S/init.mp4"') && pl.includes('\nsession/S/seg0.ts\n') && pl.includes('\n/plex/video/:/transcode/universal/session/S/seg1.ts\n'), pl);
|
||||
r = await fetch(base + '/plex/:/scrobble?key=5&identifier=com.plexapp.plugins.library', { headers: { cookie } }); ok('marking something watched (a GET) is allowed through', r.status === 200, r.status);
|
||||
|
||||
// ================= repairing itself =================
|
||||
tv.serverToken = 'SRV2'; const calls1 = tv.resourceCalls; // Plex rotates the server token
|
||||
r = await call('/plex/library/sections'); ok('a changed server token is repaired automatically (the page still loads)', r.status === 200, r.status);
|
||||
|
||||
Reference in new issue
Block a user