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>
269 lines
10 KiB
JavaScript
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);
|
|
});
|