T-HUB/infra/peertube-workers/win/e2e-peertube.mjs
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

86 lines
7.3 KiB
JavaScript

// Authorized private-video E2E. No credential is accepted in argv or printed.
import fs from 'node:fs';
import crypto from 'node:crypto';
import {execFileSync} from 'node:child_process';
const emit=x=>process.stdout.write(JSON.stringify(x)+'\n');
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
let stage='model-registration', session, db, auth, video, result={states:[]};
try {
if(JSON.parse(fs.readFileSync('/app/package.json','utf8')).version !== '7.3.0') throw Error('unreviewed server version');
const dbURL=new URL('file:///app/dist/core/initializers/database.js');
db=await import(dbURL.href);
const {logger}=await import('file:///app/dist/core/helpers/logger.js');logger.silent=true;
// Register the shipped models without invoking initDatabaseModels: that helper
// recreates a SQL function and is outside this task's authorized server scope.
const source=fs.readFileSync(dbURL,'utf8');
const names=source.match(/sequelizeTypescript\.addModels\(\[([\s\S]*?)\]\)/)[1].split(',').map(x=>x.trim()).filter(Boolean);
const imports=[...source.matchAll(/import \{ (\w+) \} from '(\.\.\/models\/[^']+)';/g)];
const models=[];
for(const name of names){const item=imports.find(m=>m[1]===name);if(!item)throw Error('model not found');models.push((await import(new URL(item[2],dbURL).href))[name]);}
db.sequelizeTypescript.addModels(models);
await db.sequelizeTypescript.authenticate();
const {OAuthTokenModel}=await import('file:///app/dist/core/models/oauth/oauth-token.js');
const {OAuthClientModel}=await import('file:///app/dist/core/models/oauth/oauth-client.js');
const {UserModel}=await import('file:///app/dist/core/models/user/user.js');
const {RunnerJobModel}=await import('file:///app/dist/core/models/runner/runner-job.js');
const {RUNNER_JOB_STATES}=await import('file:///app/dist/core/initializers/constants.js');
const {Op}=await import('sequelize');
const user=await UserModel.findOne({where:{username:'root',role:0}}), client=await OAuthClientModel.loadFirstClient();
if(!user||!client)throw Error('admin/client absent');
stage='temporary-admin-session';
const expires=new Date(Date.now()+10*60*1000);
session=await OAuthTokenModel.create({accessToken:crypto.randomBytes(32).toString('hex'),refreshToken:crypto.randomBytes(32).toString('hex'),accessTokenExpiresAt:expires,refreshTokenExpiresAt:expires,userId:user.id,oAuthClientId:client.id,loginDevice:'authorized-pt-worker-e2e',loginIP:'127.0.0.1',loginDate:new Date()});
auth={Authorization:'Bearer '+session.accessToken};
const base='http://127.0.0.1:9000/api/v1';
async function api(path,options={}) {const r=await fetch(base+path,{...options,headers:{...auth,...options.headers},signal:AbortSignal.timeout(30000)});if(!r.ok){result.http_status=r.status;throw Error('API request failed');}return r.status===204?null:r.json();}
stage='admin-auth-check';
const me=await api('/users/me');
const channel=me.videoChannels?.[0];if(!channel)throw Error('root channel absent');
const baseline=await RunnerJobModel.max('id')||0;
stage='private-upload';
const form=new FormData();
for(const [k,v] of Object.entries({channelId:String(channel.id),name:'Windows NVENC worker E2E 2026-10-09',privacy:'3',nsfw:'false',commentsEnabled:'false',downloadEnabled:'true',waitTranscoding:'true'}))form.set(k,v);
form.set('videofile',new Blob([fs.readFileSync('/tmp/pt-worker-e2e-source.mp4')],{type:'video/mp4'}),'nvenc-e2e.mp4');
const upload=await api('/videos/upload',{method:'POST',body:form});video=upload.video;
result.video={id:video.id,uuid:video.uuid,shortUUID:video.shortUUID,privacy:'private',watch_url:'https://tube.occupymedia.cc/w/'+video.shortUUID};
emit({phase:'uploaded',video:result.video});
stage='remote-job-states';
const end=Date.now()+5*60*1000, seen=new Map();let completed;
while(Date.now()<end){
const jobs=await RunnerJobModel.findAll({attributes:['id','uuid','type','state','runnerId','createdAt','startedAt','updatedAt'],where:{id:{[Op.gt]:baseline}},raw:true});
for(const j of jobs){const label=RUNNER_JOB_STATES[j.state];if(seen.get(j.uuid)!==label){seen.set(j.uuid,label);const item={...j,state:label,observed_at:new Date().toISOString()};result.states.push(item);emit({phase:'job-state',...item});}if(label==='Completed')completed=j;if(label==='Errored')throw Error('remote job errored');}
if(completed && jobs.some(j=>j.type==='vod-hls-transcoding' && RUNNER_JOB_STATES[j.state]==='Completed'))break;
await sleep(100);
}
if(!completed)throw Error('remote job deadline');
result.job={id:completed.id,uuid:completed.uuid,type:completed.type,runner_id:completed.runnerId};
stage='result-media-api';
let details;
for(let n=0;n<60;n++){details=await api('/videos/'+video.uuid);if(details.streamingPlaylists?.[0]?.files?.length)break;await sleep(500);}
const playlist=details.streamingPlaylists?.[0];if(!playlist?.files?.length)throw Error('HLS result absent');
let fileToken;
async function media(url){let u=new URL(url);u.protocol='http:';u.hostname='127.0.0.1';u.port='9000';if(fileToken)u.searchParams.set('videoFileToken',fileToken);let r=await fetch(u,{headers:auth,signal:AbortSignal.timeout(15000)});if(!r.ok&&!fileToken){try{const t=await api('/videos/'+video.uuid+'/token',{method:'POST'});fileToken=t.files?.token||t.token||t.videoFileToken;}catch{}if(fileToken){u.searchParams.set('videoFileToken',fileToken);r=await fetch(u,{headers:auth,signal:AbortSignal.timeout(15000)});}}if(!r.ok){result.media_http_status=r.status;throw Error('private media fetch failed');}return r;}
stage='playlist-playback';
const master=await (await media(playlist.playlistUrl)).text();
const renditionLine=master.split(/\r?\n/).find(l=>l.trim()&&!l.startsWith('#'));
const renditionURL=new URL(renditionLine,playlist.playlistUrl).href;
const rendition=await (await media(renditionURL)).text();
const assetLine=rendition.split(/\r?\n/).find(l=>l.trim()&&!l.startsWith('#'));
const assetURL=new URL(assetLine,renditionURL).href;
const blob=Buffer.from(await (await media(assetURL)).arrayBuffer());
const path='/tmp/pt-worker-e2e-result.mp4';fs.writeFileSync(path,blob,{mode:0o600});
const probe=JSON.parse(execFileSync('ffprobe',['-v','error','-show_streams','-show_format','-of','json',path],{encoding:'utf8',timeout:30000}));
stage='playback-decode';execFileSync('ffmpeg',['-v','error','-nostdin','-i',path,'-f','null','-'],{stdio:'pipe',timeout:30000});
const stream=probe.streams.find(s=>s.codec_type==='video');
result.playback={master_http:200,rendition_http:200,asset_http:200,decode_exit:0,bytes:blob.length,codec:stream.codec_name,width:stream.width,height:stream.height,duration:probe.format.duration};
result.success=true;emit({phase:'e2e-completed',job:result.job,playback:result.playback});
}catch(e){result.success=false;result.failed_stage=stage;result.error_type=e.name;emit({phase:'e2e-failed',stage,error_type:e.name,http_status:result.http_status,media_http_status:result.media_http_status});}
finally{
if(session){
try{await fetch('http://127.0.0.1:9000/api/v1/users/revoke-token',{method:'POST',headers:auth,signal:AbortSignal.timeout(10000)});await sleep(300);}catch{}
try{await session.destroy();result.temporary_admin_session_removed=true;}catch{result.temporary_admin_session_removed=false;}
}
fs.writeFileSync('/tmp/pt-worker-e2e-result.json',JSON.stringify(result,null,2),{mode:0o600});
emit({phase:'final-e2e-record',...result});
if(db)await db.sequelizeTypescript.close();
}