From 75666442c84110fd5aea293a6335c55f0c7b012d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Yak=C4=B1=C5=9Ft=C4=B1r?= Date: Sat, 3 Oct 2026 10:26:46 +0300 Subject: [PATCH] v1.5.7: Configurable guest video import quota, Kota Doldu modal & PeerTube login redirect --- client.js | 377 +++++++++++++++++++++++++++++++++++++++++---------- main.js | 60 +++++++- package.json | 2 +- 3 files changed, 365 insertions(+), 74 deletions(-) diff --git a/client.js b/client.js index d92d1d8..f2c5de7 100644 --- a/client.js +++ b/client.js @@ -2239,6 +2239,12 @@ try { // 4. Global Keydown Listener (Screen-Reader & Turkish Q Optimized) window.addEventListener('keydown', function (e) { if (e.key === 'Escape') { + var quotaModal = document.getElementById('yaktube-quota-modal'); + if (quotaModal) { + quotaModal.remove(); + announce('Kota uyarısı penceresi kapatıldı.'); + return; + } var sleepModal = document.getElementById('yaktube-sleep-modal'); if (sleepModal) { sleepModal.remove(); @@ -2865,85 +2871,314 @@ try { } catch (e) {} } - function handleImport(url, btn, videoTitle) { - btn.disabled = true; - btn.innerHTML = '⏳ Aktarılıyor...'; - showModal( - '[1/2] 📥 Video İndiriliyor...', - (videoTitle ? '"' + videoTitle + '" ' : 'Video ') + "YouTube'dan sunucunuza aktarılıyor. Lütfen bekleyin..." - ); + // --- Guest Video Import Quota & PeerTube Login Redirect Engine --- + var cachedGuestImportLimit = null; - fetch('/api-custom/ondemand-import', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ targetUrl: url, title: videoTitle }) - }) - .then(function (res) { - return res.json(); + function fetchGuestImportLimit() { + if (cachedGuestImportLimit !== null) { + return Promise.resolve(cachedGuestImportLimit); + } + return fetch('/api-custom/quota-config') + .then(function (r) { + return r.json(); }) - .then(function (data) { - if (!data || !data.ok) { - throw new Error(data && data.error ? data.error : 'İçe aktarma başlatılamadı.'); + .then(function (d) { + if (d && typeof d.guestImportLimit === 'number') { + cachedGuestImportLimit = d.guestImportLimit; + return cachedGuestImportLimit; } + return 5; + }) + .catch(function () { + return fetch('/plugins/yaktube-ondemand/router/quota-config') + .then(function (r2) { + return r2.json(); + }) + .then(function (d2) { + if (d2 && typeof d2.guestImportLimit === 'number') { + cachedGuestImportLimit = d2.guestImportLimit; + return cachedGuestImportLimit; + } + return 5; + }) + .catch(function () { + return 5; + }); + }); + } - var videoId = data.video.id; - var videoUuid = data.video.uuid; + // Pre-fetch guest import quota setting quietly on startup + setTimeout(function () { + fetchGuestImportLimit(); + }, 600); - var attempts = 0; - var pollInterval = setInterval(function () { - attempts++; - fetch('/api-custom/status/' + videoId) - .then(function (sRes) { - return sRes.json(); - }) - .then(function (sData) { - if (sData.isImporting) { - showModal('[1/2] 📥 Video İndiriliyor...', 'Video sunucuya indiriliyor (' + attempts * 2 + ' sn)...'); - } else if (sData.isTranscoding) { - showModal( - '[2/2] ⚙️ Oynatıcı Hazırlanıyor...', - 'Video işleniyor ve yüksek kalite akış hazırlanıyor...' - ); - } + function getGuestImportCount() { + var lsCount = 0; + var cookieCount = 0; + try { + lsCount = parseInt(localStorage.getItem('yaktube_guest_import_count') || '0', 10); + if (isNaN(lsCount) || lsCount < 0) lsCount = 0; + } catch (e) {} + try { + var match = document.cookie.match(/(?:^|;\s*)yaktube_guest_imports=(\d+)/); + if (match && match[1]) { + cookieCount = parseInt(match[1], 10); + if (isNaN(cookieCount) || cookieCount < 0) cookieCount = 0; + } + } catch (e) {} + var maxCount = Math.max(lsCount, cookieCount); + // Keep localStorage and cookie synchronized so refreshing cannot reset it + if (maxCount > 0) { + try { + localStorage.setItem('yaktube_guest_import_count', String(maxCount)); + } catch (e) {} + try { + document.cookie = 'yaktube_guest_imports=' + maxCount + '; path=/; max-age=31536000; SameSite=Lax'; + } catch (e) {} + } + return maxCount; + } - if (sData.isPublished || (sData.hasFiles && attempts >= 8)) { - clearInterval(pollInterval); - showModal('🎉 Video Hazır!', 'Video kütüphanenize eklendi. Oynatıcı açılıyor...'); - setTimeout(function () { - window.location.href = '/videos/watch/' + (sData.uuid || videoUuid); - }, 600); - } else if (sData.isFailed) { - clearInterval(pollInterval); - hideModal(); - btn.disabled = false; - btn.innerHTML = '📥 ' + escapeHtml(getInstanceName()) + "'a Aktar & İzle"; - alert('Video indirme sırasında bir hata oluştu.'); - announce('Video indirme sırasında bir hata oluştu.', true); - } - }) - .catch(function (e) { - console.error('[YakTube] Status poll error:', e); - }); + function incrementGuestImportCount() { + var nextCount = getGuestImportCount() + 1; + try { + localStorage.setItem('yaktube_guest_import_count', String(nextCount)); + } catch (e) {} + try { + document.cookie = 'yaktube_guest_imports=' + nextCount + '; path=/; max-age=31536000; SameSite=Lax'; + } catch (e) {} + return nextCount; + } - if (attempts > 35) { - clearInterval(pollInterval); + function redirectToPeerTubeLogin() { + try { + // Save current URL in sessionStorage just as PeerTube's LoginComponent does + sessionStorage.setItem('redirect-url-after-login', window.location.pathname + window.location.search); + } catch (e) {} + + // 1. Check PeerTube's native login link in header (handles redirectOnSingleExternalAuth / YakNet SSO automatically) + var loginLink = document.querySelector( + 'my-login-link a.login-button-link, my-login-link a[href], a.login-button-link' + ); + if (loginLink) { + var href = loginLink.getAttribute('href'); + if (href && href !== '#' && !href.startsWith('javascript:')) { + window.location.href = href; + return; + } + loginLink.click(); + return; + } + + // 2. Check window.PeertubeServerConfig for single external auth (PeerTube native getDefaultLoginHref logic) + try { + var cfg = window.PeertubeServerConfig; + if ( + cfg && + cfg.client && + cfg.client.menu && + cfg.client.menu.login && + cfg.client.menu.login.redirectOnSingleExternalAuth === true && + cfg.plugin && + Array.isArray(cfg.plugin.registeredExternalAuths) && + cfg.plugin.registeredExternalAuths.length === 1 + ) { + var ext = cfg.plugin.registeredExternalAuths[0]; + var npmName = ext.npmName || 'peertube-plugin-' + ext.name; + window.location.href = '/plugins/' + ext.name + '/' + ext.version + '/auth/' + ext.authName; + return; + } + } catch (e) {} + + // 3. Standard PeerTube login route fallback + window.location.href = '/login'; + } + + function showQuotaExceededModal(limitVal) { + hideModal(); + var existing = document.getElementById('yaktube-quota-modal'); + if (existing) existing.remove(); + + var modal = document.createElement('div'); + modal.id = 'yaktube-quota-modal'; + modal.className = 'yaktube-modal-overlay'; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + modal.setAttribute('aria-labelledby', 'yaktube-quota-modal-title'); + modal.setAttribute('aria-describedby', 'yaktube-quota-modal-desc'); + + var displayLimit = limitVal || cachedGuestImportLimit || getGuestImportCount() || 5; + var instanceName = escapeHtml(getInstanceName()); + + modal.innerHTML = + '
' + + '' + + '

Kota Doldu

' + + '
' + + 'Misafir kullanıcı olarak ücretsiz ' + + displayLimit + + ' video indirme sınırına ulaştınız.

' + + 'Sınırsız video aktarmaya ve izlemeye devam etmek için lütfen giriş yapın.' + + '
' + + '
' + + '' + + '' + + '
' + + '
'; + + document.body.appendChild(modal); + + var loginBtn = document.getElementById('yaktube-quota-login-btn'); + var closeBtn = document.getElementById('yaktube-quota-close-btn'); + + if (loginBtn) { + loginBtn.addEventListener('click', function () { + modal.remove(); + redirectToPeerTubeLogin(); + }); + setTimeout(function () { + loginBtn.focus(); + }, 50); + } + + if (closeBtn) { + closeBtn.addEventListener('click', function () { + modal.remove(); + announce('Kota uyarısı kapatıldı.'); + }); + } + + modal.addEventListener('click', function (e) { + if (e.target === modal) { + modal.remove(); + } + }); + + announce('Kota Doldu. Misafir video indirme sınırına ulaştınız. Devam etmek için lütfen giriş yapın.', true); + } + + function handleImport(url, btn, videoTitle) { + var loggedIn = isUserLoggedIn(); + var currentGuestCount = getGuestImportCount(); + + fetchGuestImportLimit().then(function (limit) { + if (!loggedIn && limit > 0 && currentGuestCount >= limit) { + showQuotaExceededModal(limit); + return; + } + + btn.disabled = true; + btn.innerHTML = '⏳ Aktarılıyor...'; + showModal( + '[1/2] 📥 Video İndiriliyor...', + (videoTitle ? '"' + videoTitle + '" ' : 'Video ') + "YouTube'dan sunucunuza aktarılıyor. Lütfen bekleyin..." + ); + + var headers = { 'Content-Type': 'application/json' }; + try { + var token = localStorage.getItem('access_token'); + if (token) { + headers['Authorization'] = 'Bearer ' + token; + } + } catch (e) {} + + fetch('/api-custom/ondemand-import', { + method: 'POST', + headers: headers, + body: JSON.stringify({ + targetUrl: url, + title: videoTitle, + isAuthenticated: loggedIn, + guestImportCount: currentGuestCount + }) + }) + .then(function (res) { + return res.json(); + }) + .then(function (data) { + if (data && data.quotaExceeded) { hideModal(); btn.disabled = false; btn.innerHTML = '📥 ' + escapeHtml(getInstanceName()) + "'a Aktar & İzle"; - alert( - 'İndirme arka planda tamamlanıyor. Ana sayfada veya Videolarım sayfasında kısa süre içinde hazır olacaktır.' - ); - announce('İndirme arka planda tamamlanıyor. Ana sayfada kısa süre içinde hazır olacaktır.'); + if (typeof data.limit === 'number' && data.limit > 0) { + cachedGuestImportLimit = data.limit; + } + showQuotaExceededModal(data.limit || limit); + return; } - }, 1500); - }) - .catch(function (err) { - hideModal(); - btn.disabled = false; - btn.innerHTML = '📥 ' + escapeHtml(getInstanceName()) + "'a Aktar & İzle"; - alert('İndirme hatası: ' + err.message); - announce('İndirme hatası: ' + err.message, true); - }); + + if (!data || !data.ok) { + throw new Error(data && data.error ? data.error : 'İçe aktarma başlatılamadı.'); + } + + if (!loggedIn) { + incrementGuestImportCount(); + } + + var videoId = data.video.id; + var videoUuid = data.video.uuid; + + var attempts = 0; + var pollInterval = setInterval(function () { + attempts++; + fetch('/api-custom/status/' + videoId) + .then(function (sRes) { + return sRes.json(); + }) + .then(function (sData) { + if (sData.isImporting) { + showModal( + '[1/2] 📥 Video İndiriliyor...', + 'Video sunucuya indiriliyor (' + attempts * 2 + ' sn)...' + ); + } else if (sData.isTranscoding) { + showModal( + '[2/2] ⚙️ Oynatıcı Hazırlanıyor...', + 'Video işleniyor ve yüksek kalite akış hazırlanıyor...' + ); + } + + if (sData.isPublished || (sData.hasFiles && attempts >= 8)) { + clearInterval(pollInterval); + showModal('🎉 Video Hazır!', 'Video kütüphanenize eklendi. Oynatıcı açılıyor...'); + setTimeout(function () { + window.location.href = '/videos/watch/' + (sData.uuid || videoUuid); + }, 600); + } else if (sData.isFailed) { + clearInterval(pollInterval); + hideModal(); + btn.disabled = false; + btn.innerHTML = '📥 ' + escapeHtml(getInstanceName()) + "'a Aktar & İzle"; + alert('Video indirme sırasında bir hata oluştu.'); + announce('Video indirme sırasında bir hata oluştu.', true); + } + }) + .catch(function (e) { + console.error('[YakTube] Status poll error:', e); + }); + + if (attempts > 35) { + clearInterval(pollInterval); + hideModal(); + btn.disabled = false; + btn.innerHTML = '📥 ' + escapeHtml(getInstanceName()) + "'a Aktar & İzle"; + alert( + 'İndirme arka planda tamamlanıyor. Ana sayfada veya Videolarım sayfasında kısa süre içinde hazır olacaktır.' + ); + announce('İndirme arka planda tamamlanıyor. Ana sayfada kısa süre içinde hazır olacaktır.'); + } + }, 1500); + }) + .catch(function (err) { + hideModal(); + btn.disabled = false; + btn.innerHTML = '📥 ' + escapeHtml(getInstanceName()) + "'a Aktar & İzle"; + alert('İndirme hatası: ' + err.message); + announce('İndirme hatası: ' + err.message, true); + }); + }); } function checkAndInjectSearchResults() { @@ -3088,6 +3323,9 @@ try { .then(function (responses) { var ytData = responses[0] || {}; var localData = responses[1] || {}; + if (typeof ytData.guestImportLimit === 'number') { + cachedGuestImportLimit = ytData.guestImportLimit; + } var rawYtResults = ytData.results || []; var uniqueYtResults = []; @@ -4359,6 +4597,9 @@ try { return res.json(); }) .then(function (data) { + if (data && typeof data.guestImportLimit === 'number') { + cachedGuestImportLimit = data.guestImportLimit; + } var rawResults = data.results || []; var normalizedCurrentTitle = rawTitle.toLowerCase().trim(); diff --git a/main.js b/main.js index 2811505..bf8fa57 100644 --- a/main.js +++ b/main.js @@ -182,19 +182,55 @@ async function getAdminToken() { }); } -async function register({ getRouter, peertubeHelpers, logger }) { +async function register({ getRouter, registerSetting, settingsManager, peertubeHelpers, logger }) { const router = getRouter(); const ytdlpPath = resolveYtDlpPath(logger); + let guestImportLimit = 5; + + if (typeof registerSetting === 'function') { + registerSetting({ + name: 'guest-import-limit', + label: 'Misafir Video İndirme Sınırı (Kota)', + type: 'input', + description: + 'Giriş yapmamış (misafir) kullanıcıların YouTube üzerinden sunucuya aktarabileceği maksimum video sayısı (Örn: 5). Sınır istemiyorsanız (sınırsız) 0 yazın.', + private: false, + default: '5' + }); + } + + async function loadPluginSettings() { + if (!settingsManager || typeof settingsManager.getSetting !== 'function') return; + try { + const val = await settingsManager.getSetting('guest-import-limit'); + if (val !== undefined && val !== null && String(val).trim() !== '') { + const parsed = parseInt(String(val).trim(), 10); + guestImportLimit = isNaN(parsed) || parsed < 0 ? 5 : parsed; + } else { + guestImportLimit = 5; + } + } catch (e) {} + } + + await loadPluginSettings(); + if (settingsManager && typeof settingsManager.onSettingsChange === 'function') { + settingsManager.onSettingsChange(loadPluginSettings); + } + + router.get('/quota-config', (_req, res) => { + return res.json({ ok: true, guestImportLimit }); + }); + router.get('/search', async (req, res) => { const query = (req.query.q || '').trim(); if (!query || query.length < 2) { - return res.json({ results: [] }); + return res.json({ results: [], guestImportLimit }); } const cached = getCached(query); if (cached) { - return res.json({ results: cached, cached: true }); + return res.json({ results: cached, cached: true, guestImportLimit }); } const args = ['ytsearch6:' + query, '--dump-single-json', '--flat-playlist', '--skip-download', '--no-warnings']; @@ -233,7 +269,7 @@ async function register({ getRouter, peertubeHelpers, logger }) { }); setCache(query, results); - return res.json({ results, cached: false }); + return res.json({ results, cached: false, guestImportLimit }); } catch (parseErr) { if (logger) logger.error('JSON parse error in search: ' + parseErr.message); return res.status(500).json({ error: 'Failed to parse search results' }); @@ -242,11 +278,25 @@ async function register({ getRouter, peertubeHelpers, logger }) { }); router.post('/import', async (req, res) => { - const { targetUrl, channelId, category } = req.body; + const { targetUrl, channelId, category, isAuthenticated, guestImportCount } = req.body; if (!targetUrl || (!targetUrl.includes('youtube.com') && !targetUrl.includes('youtu.be'))) { return res.status(400).json({ error: 'Invalid YouTube URL' }); } + const authHeader = req.headers && req.headers.authorization; + const isLoggedUser = Boolean(isAuthenticated || (authHeader && authHeader.startsWith('Bearer '))); + if (!isLoggedUser && guestImportLimit > 0) { + const clientCount = parseInt(String(guestImportCount || '0'), 10) || 0; + if (clientCount >= guestImportLimit) { + return res.status(429).json({ + ok: false, + quotaExceeded: true, + limit: guestImportLimit, + error: 'Kota Doldu: Lütfen devam etmek için giriş yapın.' + }); + } + } + try { const token = await getAdminToken(); const resolvedChannelId = channelId ? String(channelId) : await getDefaultChannelId(category); diff --git a/package.json b/package.json index ed948b7..996cca6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peertube-plugin-yaktube-ondemand", - "version": "1.5.6", + "version": "1.5.7", "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",