commit fd97a19cc1b8714a749b9c177645c45455ceb53d Author: Administrator Date: Tue Mar 17 05:53:42 2026 +0100 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..975b41b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.env +byse-candidates.csv diff --git a/index.js b/index.js new file mode 100644 index 0000000..e47b314 --- /dev/null +++ b/index.js @@ -0,0 +1,268 @@ +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); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..ddc0df5 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "byse-video-manager", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "dotenv": "^16.4.7" + } +}