byse-video-manager/index.js
Administrator fd97a19cc1 feat: byse.sx video manager - scan & cleanup tool
Scans all videos via internal byse.sx API, identifies videos
with 0 views older than 3 months, and allows batch deletion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 05:53:42 +01:00

269 lines
10 KiB
JavaScript

import 'dotenv/config';
import readline from 'readline';
import fs from 'fs';
const SID = process.env.BYSE_SID;
if (!SID) { console.error('BYSE_SID fehlt in .env'); process.exit(1); }
const COOKIE = `sid=${SID}`;
const BASE = 'https://byse.sx/api';
const PER_PAGE = 500;
const DELETE_BATCH = 100;
const THREE_MONTHS_AGO = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
// ── Helpers ──
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB';
return (bytes / 1073741824).toFixed(2) + ' GB';
}
function formatAge(dateStr) {
const days = Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000);
if (days < 30) return `${days}d`;
if (days < 365) return `${Math.floor(days / 30)}mo`;
return `${(days / 365).toFixed(1)}y`;
}
function ask(question) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise(r => rl.question(question, a => { rl.close(); r(a.trim()); }));
}
async function apiFetch(path) {
const res = await fetch(`${BASE}${path}`, {
headers: { cookie: COOKIE, 'content-type': 'application/json' }
});
if (res.status === 401 || res.status === 403) {
console.error('\nSession abgelaufen! Hol dir einen neuen SID-Cookie aus dem Browser.');
process.exit(1);
}
return res.json();
}
async function apiDelete(fileIds) {
const res = await fetch(`${BASE}/del_file`, {
method: 'POST',
headers: { cookie: COOKIE, 'content-type': 'application/json' },
body: JSON.stringify({ file_ids: fileIds })
});
return res.json();
}
function progressBar(current, total, width = 30) {
const pct = Math.floor((current / total) * 100);
const filled = Math.floor((current / total) * width);
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
return `[${bar}] ${pct}% (${current}/${total})`;
}
// ── Scan ──
async function scanAllVideos() {
console.log('\nVerbindung pruefen...');
const overview = await apiFetch('/my_files_overview');
if (overview.status !== 'ok') {
console.error('API-Fehler:', overview);
process.exit(1);
}
const totalFiles = overview.totals.files;
console.log(`Gefunden: ${totalFiles.toLocaleString('de-DE')} Videos\n`);
const allFiles = [];
let cursor = null;
let batch = 0;
const totalPages = Math.ceil(totalFiles / PER_PAGE);
while (true) {
batch++;
process.stdout.write(`\rLade Videos... ${progressBar(Math.min(batch, totalPages), totalPages)} `);
try {
const url = cursor
? `/my_files_files?folder_id=0&per_page=${PER_PAGE}&cursor=${encodeURIComponent(cursor)}`
: `/my_files_files?folder_id=0&per_page=${PER_PAGE}`;
const data = await apiFetch(url);
if (!data.files || data.files.length === 0) break;
allFiles.push(...data.files);
cursor = data.file_pagination?.next_cursor;
if (!cursor) break;
} catch (err) {
console.warn(`\nFehler bei Batch ${batch}, ueberspringe... (${err.message})`);
break;
}
// kleine Pause um Rate-Limits zu vermeiden
if (batch % 10 === 0) await new Promise(r => setTimeout(r, 200));
}
process.stdout.write('\n');
return allFiles;
}
// ── Filter ──
function filterCandidates(files) {
return files.filter(f => {
const created = new Date(f.created);
return f.views === 0 && created < THREE_MONTHS_AGO;
}).sort((a, b) => new Date(a.created) - new Date(b.created)); // aelteste zuerst
}
// ── Display ──
function showSummary(candidates, totalFiles) {
const totalSize = candidates.reduce((sum, f) => sum + (f.size || 0), 0);
const oldest = candidates[0]?.created?.split('T')[0] || '-';
const newest = candidates[candidates.length - 1]?.created?.split('T')[0] || '-';
console.log('\n════════════════════════════════════════════════');
console.log(' ERGEBNIS');
console.log('════════════════════════════════════════════════');
console.log(` Gesamt Videos: ${totalFiles.toLocaleString('de-DE')}`);
console.log(` Kandidaten (0 Views,`);
console.log(` aelter als 3 Mon.): ${candidates.length.toLocaleString('de-DE')}`);
console.log(` Speicher freigeben: ${formatSize(totalSize)}`);
console.log(` Aeltestes Video: ${oldest}`);
console.log(` Neuestes Kandidat: ${newest}`);
console.log('════════════════════════════════════════════════\n');
}
function showTable(candidates, offset = 0, limit = 50) {
const slice = candidates.slice(offset, offset + limit);
const header = `${'#'.padStart(6)} ${'Titel'.padEnd(55)} ${'Alter'.padStart(6)} ${'Groesse'.padStart(10)} ${'Views'.padStart(5)}`;
console.log(header);
console.log('─'.repeat(header.length));
for (let i = 0; i < slice.length; i++) {
const f = slice[i];
const idx = (offset + i + 1).toString().padStart(6);
const title = (f.title || f.name || '???').substring(0, 55).padEnd(55);
const age = formatAge(f.created).padStart(6);
const size = formatSize(f.size || 0).padStart(10);
const views = String(f.views).padStart(5);
console.log(`${idx} ${title} ${age} ${size} ${views}`);
}
if (offset + limit < candidates.length) {
console.log(`\n... und ${(candidates.length - offset - limit).toLocaleString('de-DE')} weitere`);
}
}
// ── Delete ──
async function deleteVideos(candidates) {
const total = candidates.length;
let deleted = 0;
let errors = 0;
console.log(`\nLoesche ${total.toLocaleString('de-DE')} Videos in ${Math.ceil(total / DELETE_BATCH)} Batches...\n`);
for (let i = 0; i < total; i += DELETE_BATCH) {
const batch = candidates.slice(i, i + DELETE_BATCH);
const ids = batch.map(f => f.id);
process.stdout.write(`\rLoeschen... ${progressBar(Math.min(i + DELETE_BATCH, total), total)} `);
try {
const result = await apiDelete(ids);
if (result.status === 'ok') {
deleted += batch.length;
} else {
errors += batch.length;
console.warn(`\nBatch-Fehler: ${result.message || result.error}`);
}
} catch (err) {
errors += batch.length;
console.warn(`\nNetzwerk-Fehler: ${err.message}`);
}
// Pause zwischen Batches
await new Promise(r => setTimeout(r, 500));
}
process.stdout.write('\n');
console.log(`\nFertig! Geloescht: ${deleted.toLocaleString('de-DE')} | Fehler: ${errors}`);
}
// ── CSV Export ──
function exportCSV(candidates, filename = 'byse-candidates.csv') {
const header = 'ID,Code,Titel,Views,Erstellt,Groesse_MB,Alter_Tage\n';
const rows = candidates.map(f => {
const days = Math.floor((Date.now() - new Date(f.created).getTime()) / 86400000);
const sizeMB = ((f.size || 0) / 1048576).toFixed(1);
const title = (f.title || '').replace(/"/g, '""');
return `${f.id},"${f.code}","${title}",${f.views},${f.created},${sizeMB},${days}`;
}).join('\n');
fs.writeFileSync(filename, header + rows, 'utf8');
console.log(`CSV exportiert: ${filename} (${candidates.length} Eintraege)`);
}
// ── Main ──
async function main() {
console.log('╔══════════════════════════════════════╗');
console.log('║ Byse.sx Video Manager ║');
console.log('║ Videos ohne Views aufräumen ║');
console.log('╚══════════════════════════════════════╝');
const allFiles = await scanAllVideos();
const candidates = filterCandidates(allFiles);
if (candidates.length === 0) {
console.log('\nKeine Videos gefunden die aelter als 3 Monate sind UND 0 Views haben.');
process.exit(0);
}
showSummary(candidates, allFiles.length);
let offset = 0;
while (true) {
console.log('\nOptionen:');
console.log(' [1] Liste anzeigen (naechste 50)');
console.log(' [2] CSV exportieren');
console.log(' [3] ALLE Kandidaten loeschen');
console.log(' [4] Nur Videos aelter als X Monate loeschen');
console.log(' [5] Beenden');
const choice = await ask('\nAuswahl: ');
if (choice === '1') {
showTable(candidates, offset, 50);
offset += 50;
if (offset >= candidates.length) offset = 0;
} else if (choice === '2') {
exportCSV(candidates);
} else if (choice === '3') {
const confirm = await ask(`\nSICHER? ${candidates.length.toLocaleString('de-DE')} Videos werden UNWIDERRUFLICH geloescht!\nTippe "LOESCHEN" zum Bestaetigen: `);
if (confirm === 'LOESCHEN') {
await deleteVideos(candidates);
break;
} else {
console.log('Abgebrochen.');
}
} else if (choice === '4') {
const months = await ask('Mindest-Alter in Monaten (z.B. 6): ');
const m = parseInt(months);
if (isNaN(m) || m < 1) { console.log('Ungueltige Eingabe.'); continue; }
const cutoff = new Date(Date.now() - m * 30 * 24 * 60 * 60 * 1000);
const filtered = candidates.filter(f => new Date(f.created) < cutoff);
console.log(`\n${filtered.length.toLocaleString('de-DE')} Videos aelter als ${m} Monate mit 0 Views.`);
const totalSize = filtered.reduce((sum, f) => sum + (f.size || 0), 0);
console.log(`Speicher: ${formatSize(totalSize)}`);
if (filtered.length === 0) continue;
showTable(filtered, 0, 30);
const confirm = await ask(`\nTippe "LOESCHEN" um diese ${filtered.length.toLocaleString('de-DE')} Videos zu loeschen: `);
if (confirm === 'LOESCHEN') {
await deleteVideos(filtered);
break;
} else {
console.log('Abgebrochen.');
}
} else if (choice === '5') {
console.log('Bye!');
break;
}
}
}
main().catch(err => {
console.error('Fehler:', err.message);
process.exit(1);
});