From aa9c4c76c89133b574060c37adbca4d6e3912978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Yak=C4=B1=C5=9Ft=C4=B1r?= Date: Sat, 3 Oct 2026 11:14:03 +0300 Subject: [PATCH] feat(storage): add configurable max-disk-usage-percent auto-pruner setting & update APK download filename (v1.5.9) --- client.js | 4 +- main.js | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 3 files changed, 205 insertions(+), 6 deletions(-) diff --git a/client.js b/client.js index 5245031..c56d6e1 100644 --- a/client.js +++ b/client.js @@ -4962,7 +4962,7 @@ try { apkBtn.id = 'yaktube-apk-download-btn'; apkBtn.href = '/yaktube.apk'; apkBtn.className = 'yaktube-apk-btn'; - apkBtn.setAttribute('download', 'YakTube_Mobile_v1.0.apk'); + apkBtn.setAttribute('download', 'YakTube.apk'); apkBtn.setAttribute('aria-label', 'YakTube Android APK İndir (4.5 MB)'); apkBtn.style.cssText = 'background: #1e293b; color: #ff8f37; border: 1px solid #ff8f37; padding: 6px 12px; border-radius: 20px; font-weight: 700; font-size: 11px; text-decoration: none; display: inline-flex; align-items: center; gap: 4px; margin-right: 6px; transition: all 0.2s; box-shadow: 0 4px 12px rgba(0,0,0,0.3);'; @@ -4996,7 +4996,7 @@ try { sidebarLink.id = 'yaktube-sidebar-apk-link'; sidebarLink.href = '/yaktube.apk'; sidebarLink.className = 'menu-link item-link'; - sidebarLink.setAttribute('download', 'YakTube_Mobile_v1.0.apk'); + sidebarLink.setAttribute('download', 'YakTube.apk'); sidebarLink.setAttribute('aria-label', 'YakTube Android Uygulamasını İndir (4.5 MB)'); sidebarLink.style.cssText = 'display: flex; align-items: center; gap: 10px; padding: 10px 16px; margin: 8px 12px; background: rgba(255, 143, 55, 0.1); color: #ff8f37; border: 1px solid rgba(255, 143, 55, 0.3); border-radius: 8px; font-weight: 700; font-size: 13px; text-decoration: none;'; diff --git a/main.js b/main.js index adb8585..493cef7 100644 --- a/main.js +++ b/main.js @@ -95,10 +95,13 @@ function postInnerTubeJSON(url, data) { }); } +let pruneIntervalTimer = null; + async function register({ getRouter, registerSetting, settingsManager, peertubeHelpers, logger }) { const router = getRouter(); let guestImportLimit = 5; + let maxDiskUsagePercent = 85; let customChannelId = ''; let customYtDlpPath = ''; @@ -113,6 +116,16 @@ async function register({ getRouter, registerSetting, settingsManager, peertubeH default: '5' }); + registerSetting({ + name: 'max-disk-usage-percent', + label: 'Otomatik Disk Temizleme Sınırı (%)', + type: 'input', + description: + 'Sunucu diski bu doluluk yüzdesine (Örn: 85) ulaştığında, en eski ve en az izlenen videolar otomatik olarak silinerek yer açılır. Otomatik silmeyi kapatmak için 0 yazın.', + private: false, + default: '85' + }); + registerSetting({ name: 'default-channel-id', label: 'Varsayılan Video Kanalı ID (Opsiyonel)', @@ -146,6 +159,16 @@ async function register({ getRouter, registerSetting, settingsManager, peertubeH } } catch (e) {} + try { + const diskVal = await settingsManager.getSetting('max-disk-usage-percent'); + if (diskVal !== undefined && diskVal !== null && String(diskVal).trim() !== '') { + const parsedDisk = parseInt(String(diskVal).trim(), 10); + maxDiskUsagePercent = isNaN(parsedDisk) || parsedDisk < 0 ? 85 : Math.min(99, parsedDisk); + } else { + maxDiskUsagePercent = 85; + } + } catch (e) {} + try { const chVal = await settingsManager.getSetting('default-channel-id'); customChannelId = chVal ? String(chVal).trim() : ''; @@ -404,10 +427,178 @@ async function register({ getRouter, registerSetting, settingsManager, peertubeH } } - // 1. Quota Config Route + // Universal Disk Storage & Auto-Pruner (Works on ANY PeerTube server without bridge.js) + const MIN_FREE_GB = 25; + const MIN_VIDEO_AGE_MS = 7 * 24 * 60 * 60 * 1000; + + function getStorageCheckPath() { + try { + if ( + peertubeHelpers && + peertubeHelpers.plugin && + typeof peertubeHelpers.plugin.getDataDirectoryPath === 'function' + ) { + let dir = peertubeHelpers.plugin.getDataDirectoryPath(); + while (dir && dir !== path.dirname(dir)) { + if (fs.existsSync(dir)) return dir; + dir = path.dirname(dir); + } + } + } catch (e) {} + if (process.platform === 'win32' && fs.existsSync('D:\\yaktube_storage')) { + return 'D:\\'; + } + return process.cwd(); + } + + async function getStorageStats() { + await loadPluginSettings(); + const checkPath = getStorageCheckPath(); + return new Promise((resolve, reject) => { + fs.statfs(checkPath, (err, stats) => { + if (err) return reject(err); + const totalBytes = stats.blocks * stats.bsize; + const freeBytes = stats.bfree * stats.bsize; + const usedBytes = totalBytes - freeBytes; + const totalGB = totalBytes / (1024 * 1024 * 1024); + const freeGB = freeBytes / (1024 * 1024 * 1024); + const usedGB = usedBytes / (1024 * 1024 * 1024); + const usedPercent = totalBytes > 0 ? (usedBytes / totalBytes) * 100 : 0; + const isLowSpace = maxDiskUsagePercent > 0 && (usedPercent >= maxDiskUsagePercent || freeGB < MIN_FREE_GB); + resolve({ + totalBytes, + freeBytes, + usedBytes, + totalGB: Number(totalGB.toFixed(2)), + freeGB: Number(freeGB.toFixed(2)), + usedGB: Number(usedGB.toFixed(2)), + usedPercent: Number(usedPercent.toFixed(1)), + maxDiskUsagePercent, + isLowSpace + }); + }); + }); + } + + async function checkAndPruneStorage() { + try { + let stats = await getStorageStats(); + if (maxDiskUsagePercent <= 0) { + return { pruned: 0, freedGB: 0, status: 'Auto-pruning disabled (max-disk-usage-percent = 0)' }; + } + if (!stats.isLowSpace) { + return { + pruned: 0, + freedGB: 0, + status: `Disk space healthy (${stats.freeGB} GB free, ${stats.usedPercent}% used, limit: ${maxDiskUsagePercent}%)` + }; + } + + const targetPercent = Math.max(10, maxDiskUsagePercent - 10); + if (logger) { + logger.info( + `[YakTube Auto-Pruner] Low disk space detected! Used: ${stats.usedPercent}% (Limit: ${maxDiskUsagePercent}%), Free: ${stats.freeGB} GB. Starting cleanup...` + ); + } + + const token = await getAdminToken(); + const { hostname, port, hostHeader } = getLocalServerTarget(); + const vids = await dbQuery( + `SELECT id, uuid, name, views, "createdAt" + FROM "video" + WHERE remote = false + ORDER BY views ASC, "createdAt" ASC + LIMIT 100` + ); + + const now = Date.now(); + let prunedCount = 0; + + for (const vid of vids) { + const createdAt = new Date(vid.createdAt).getTime(); + if (now - createdAt < MIN_VIDEO_AGE_MS) { + continue; + } + + if (logger) { + logger.info(`[YakTube Auto-Pruner] Pruning unused video: "${vid.name}" (ID: ${vid.id}, Views: ${vid.views})`); + } + + await new Promise(resolve => { + const delReq = http.request( + { + hostname, + port, + path: '/api/v1/videos/' + vid.id, + method: 'DELETE', + headers: { + Host: hostHeader, + Authorization: 'Bearer ' + token + } + }, + delRes => { + delRes.on('data', () => {}); + delRes.on('end', resolve); + } + ); + delReq.on('error', resolve); + delReq.end(); + }); + + prunedCount++; + stats = await getStorageStats(); + if (stats.usedPercent <= targetPercent && stats.freeGB >= MIN_FREE_GB) { + if (logger) { + logger.info( + `[YakTube Auto-Pruner] Target disk space reached (${stats.usedPercent}% used). Pruning completed.` + ); + } + break; + } + } + + return { pruned: prunedCount, status: 'Pruning completed', currentStats: stats }; + } catch (err) { + if (logger) logger.error('[YakTube Auto-Pruner] Error during prune check: ' + err.message); + return { error: err.message }; + } + } + + if (pruneIntervalTimer) clearInterval(pruneIntervalTimer); + pruneIntervalTimer = setInterval( + () => { + checkAndPruneStorage().catch(() => {}); + }, + 4 * 60 * 60 * 1000 + ); + + // 1. Quota & Storage Config Routes router.get('/quota-config', async (_req, res) => { await loadPluginSettings(); - return res.json({ ok: true, guestImportLimit }); + return res.json({ ok: true, guestImportLimit, maxDiskUsagePercent }); + }); + + router.get('/storage-status', async (_req, res) => { + try { + const stats = await getStorageStats(); + return res.json({ + ok: true, + stats, + policy: { + maxUsagePercent: maxDiskUsagePercent, + targetUsagePercent: maxDiskUsagePercent > 0 ? Math.max(10, maxDiskUsagePercent - 10) : 0, + minFreeGB: MIN_FREE_GB, + gracePeriodDays: 7 + } + }); + } catch (e) { + return res.status(500).json({ error: e.message }); + } + }); + + router.post('/prune-check', async (_req, res) => { + const result = await checkAndPruneStorage(); + return res.json(result); }); // 2. YouTube Search Route (aliases: /youtube-search and /search) @@ -504,6 +695,9 @@ async function register({ getRouter, registerSetting, settingsManager, peertubeH } } + // Non-blocking background check to prune old videos if disk usage exceeds maxDiskUsagePercent + checkAndPruneStorage().catch(() => {}); + try { const token = await getAdminToken(); const resolvedChannelId = await resolveTargetChannelId(title, channelId); @@ -863,7 +1057,12 @@ async function register({ getRouter, registerSetting, settingsManager, peertubeH }); } -async function unregister() {} +async function unregister() { + if (pruneIntervalTimer) { + clearInterval(pruneIntervalTimer); + pruneIntervalTimer = null; + } +} module.exports = { register, diff --git a/package.json b/package.json index c1b21f7..489baad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peertube-plugin-yaktube-ondemand", - "version": "1.5.8", + "version": "1.5.9", "description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations, Sleep Timer, Touch Gestures, Voice Assistant & A11y Engine for PeerTube", "main": "main.js", "library": "./main.js",