From b31f508702aa49cf525aa1aee54a16cafae61e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enes=20Yak=C4=B1=C5=9Ft=C4=B1r?= Date: Sat, 29 Aug 2026 12:42:36 +0300 Subject: [PATCH] feat: initial release of Accessible On-Demand and Smart Recommendations v1.0.0 --- .gitignore | 4 + LICENSE | 20 + README.md | 62 ++ client.js | 2057 ++++++++++++++++++++++++++++++++++++++++++++++++++ main.js | 267 +++++++ package.json | 39 + style.css | 407 ++++++++++ 7 files changed, 2856 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 client.js create mode 100644 main.js create mode 100644 package.json create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ca7ce1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +*.log +.DS_Store +dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..558a2d7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2026 Enes Yakıştır (YakNet Ecosystem) + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..221839c --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# PeerTube Plugin: On-Demand Search & Smart Recommendations (`peertube-plugin-yaktube-ondemand`) + +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +[![PeerTube Engine](https://img.shields.io/badge/PeerTube-%3E%3D5.0.0-orange.svg)](https://joinpeertube.org) +[![Accessibility: 100%](https://img.shields.io/badge/Accessibility-WCAG%202.1%20AA-green.svg)](https://yaktube.yakhub.com.tr) + +**English** | [Türkçe](#türkçe) + +`peertube-plugin-yaktube-ondemand` enhances any [PeerTube](https://joinpeertube.org) instance with **live YouTube on-demand search**, **one-click import & instant watch**, **hybrid related video recommendations**, and **comprehensive screen reader accessibility (TalkBack / NVDA)**. + +--- + +## 🌟 Key Features + +- **⚡ Live Web & YouTube On-Demand Search:** Seamlessly search YouTube directly within your instance search bar. If the video is already on your instance, it plays immediately; otherwise, users can import it in 1 click. +- **🔥 Hybrid Smart Recommendations:** Intelligently discovers related content across local federated videos first, supplemented with relevant live discoveries. +- **♿ Full Screen Reader Accessibility (A11y):** + - Dynamic result counters with live polite announcements (`aria-live`). + - Accessible keyboard navigation modal (`?` shortcut). + - Auto-labeled player controls, buttons, and thumbnails. +- **💬 Dynamic Live YouTube Comments:** View original YouTube community comments under imported videos on demand. +- **🌐 Dynamic Multi-Instance Branding:** Automatically adapts to any instance name (e.g., _YakTube_, _Framatube_, _TubeLibre_). + +--- + +## 📦 Installation + +### Option 1: Via PeerTube Admin Interface (Recommended) + +1. In your PeerTube administration dashboard, navigate to: **Administration > Plugins / Themes**. +2. Search for `peertube-plugin-yaktube-ondemand`. +3. Click **Install**. + +### Option 2: Via Command Line + +```bash +cd /var/www/peertube/peertube-latest +NODE_ENV=production npm run plugin:install -- --plugin-path peertube-plugin-yaktube-ondemand +``` + +--- + + + +## 🇹🇷 Türkçe Açıklama + +`peertube-plugin-yaktube-ondemand`, PeerTube sunucuları için geliştirilmiş **Canlı YouTube Arama, Tek Tıkla İndir & İzle ve Akıllı Hibrit Öneri** eklentisidir. + +### Özellikler + +- **⚡ Canlı Arama ve İndir-İzle:** Arama çubuğuna yazılan konuyu YouTube üzerinde tarar, sunucuda mevcutsa hemen oynatır, değilse tek tıkla arka planda sunucuya indirip başlatır. +- **🔥 Akıllı Hibrit Önerilen Videolar:** İzlenen videonun sanatçı ve konusunu ayıklayarak Fediverse kütüphanesindeki ve webdeki en alakalı 20+ videoyu listeler. +- **♿ %100 Ekran Okuyucu Desteği:** TalkBack, NVDA ve Jaws kullanıcıları için dinamik sesli sayaç, kısayol yardım penceresi ve erişilebilir kontroller. +- **💬 Orijinal YouTube Yorumları:** İçe aktarılan videoların orijinal topluluk yorumlarını izleme sayfasında anlık gösterir. + +--- + +## 📄 License + +This project is licensed under the [GNU Affero General Public License v3.0](LICENSE). + +Developed with ❤️ by **Enes Yakıştır** and the **YakNet** Community. diff --git a/client.js b/client.js new file mode 100644 index 0000000..0e814c0 --- /dev/null +++ b/client.js @@ -0,0 +1,2057 @@ +(function () { + if (window.__YAKTUBE_ENGINE_LOADED__) return; + window.__YAKTUBE_ENGINE_LOADED__ = true; + console.log('[YakTube] YakNet SSO, Custom Branding, A11y & On-Demand Engine Active'); + + // Dynamic Instance Name Helper + function getInstanceName() { + if ( + window.PeertubeServerConfig && + window.PeertubeServerConfig.instance && + window.PeertubeServerConfig.instance.name + ) { + return window.PeertubeServerConfig.instance.name; + } + var metaSite = document.querySelector('meta[property="og:site_name"]'); + if (metaSite && metaSite.content) return metaSite.content; + var brandEl = document.querySelector('.header-left .brand-name, .instance-name, a.brand'); + if (brandEl && brandEl.textContent) return brandEl.textContent.trim(); + return 'YakTube'; + } + + // 1. Screen Reader Live Announcer + var liveRegion = null; + function getLiveRegion() { + if (!liveRegion || !document.body.contains(liveRegion)) { + liveRegion = document.getElementById('yaktube-a11y-live-region'); + if (!liveRegion) { + liveRegion = document.createElement('div'); + liveRegion.id = 'yaktube-a11y-live-region'; + liveRegion.setAttribute('aria-live', 'polite'); + liveRegion.setAttribute('aria-atomic', 'true'); + liveRegion.className = 'yaktube-sr-only'; + document.body.appendChild(liveRegion); + } + } + return liveRegion; + } + + function announce(text, assertive) { + var region = getLiveRegion(); + if (assertive) { + region.setAttribute('aria-live', 'assertive'); + } else { + region.setAttribute('aria-live', 'polite'); + } + region.textContent = ''; + setTimeout(function () { + region.textContent = text; + }, 50); + } + + function triggerYakNetOAuth() { + sessionStorage.removeItem('yaknet_manual_logout'); + var callbackUrl = 'https://yaktube.yakhub.com.tr/plugins/peertube-plugin-auth-yaknet/router/auth-callback'; + window.location.href = + 'https://auth.yakhub.com.tr/oauth/authorize?client_id=01a03c41-f758-721e-b927-619bffde5c23&redirect_uri=' + + encodeURIComponent(callbackUrl) + + '&response_type=code&scope='; + } + + // Handle direct postMessage from YakNet SSO frame + window.addEventListener('message', function (ev) { + if (ev.data && ev.data.type === 'YAKNET_SSO_STATUS') { + if (ev.data.authenticated && ev.data.user) { + var token = localStorage.getItem('access_token'); + if ( + !token && + !window.location.search.includes('local=true') && + !window.location.search.includes('externalAuthError') + ) { + sessionStorage.removeItem('yaknet_manual_logout'); + console.log('[YakTube] Live YakNet session detected via SSO frame! Connecting...', ev.data.user); + triggerYakNetOAuth(); + } + } else if (ev.data.authenticated === false) { + var hasToken = localStorage.getItem('access_token') || localStorage.getItem('username'); + if (hasToken) { + console.log('[YakTube] YakNet session ended elsewhere. Clearing YakTube...'); + try { + localStorage.clear(); + sessionStorage.clear(); + } catch (e) {} + window.location.reload(); + } + } + } + }); + + // Global listener for YakNet SSO broadcast from auth.yakhub.com.tr + window.addEventListener('yaknet-auth', function (e) { + if (e.detail && e.detail.authenticated && e.detail.user) { + var token = localStorage.getItem('access_token'); + if ( + !token && + !window.location.search.includes('local=true') && + !window.location.search.includes('externalAuthError') + ) { + sessionStorage.removeItem('yaknet_manual_logout'); + console.log('[YakTube] Initiating automatic single-sign-on handshake for:', e.detail.user); + triggerYakNetOAuth(); + } + } else if (e.detail && e.detail.authenticated === false) { + var hasToken = localStorage.getItem('access_token') || localStorage.getItem('username'); + if (hasToken) { + try { + localStorage.clear(); + sessionStorage.clear(); + } catch (err) {} + window.location.reload(); + } + } + }); + + // Unified single sign-out for YakTube and YakNet + function performUnifiedLogout() { + try { + localStorage.clear(); + sessionStorage.clear(); + sessionStorage.setItem('yaknet_manual_logout', 'true'); + } catch (err) {} + window.location.replace( + 'https://auth.yakhub.com.tr/sdk/logout?redirect_uri=' + encodeURIComponent('https://yaktube.yakhub.com.tr') + ); + } + + document.addEventListener( + 'click', + function (e) { + var el = e.target; + if (el && el.closest) { + var isLogout = el.closest( + 'a[href*="logout"], button[title*="Log out"], button[title*="Çıkış"], .logout-button, [class*="logout"], [aria-label*="Çıkış"], [aria-label*="Log out"]' + ); + if (isLogout) { + e.preventDefault(); + e.stopPropagation(); + performUnifiedLogout(); + return; + } + var isLogin = el.closest( + 'my-login-link, .login-button, a[href*="/login"]:not(#yaktube-show-admin-login), a[aria-label*="Giriş"], a[title*="Giriş"], a[title*="login"], a[aria-label*="login"], .yaknet-sso-login-btn' + ); + if (isLogin && !window.location.search.includes('local=true')) { + e.preventDefault(); + e.stopPropagation(); + sessionStorage.removeItem('yaknet_manual_logout'); + triggerYakNetOAuth(); + return; + } + } + }, + true + ); + + // 2. YakNet SSO Header & Login Flow + function injectYakNetSSO() { + // 2.1 Header: Place YakNet Account widget in top-right .buttons-container + var headerRight = + document.querySelector('my-header .buttons-container') || + document.querySelector('.buttons-container') || + document.querySelector('my-header .header-right') || + document.querySelector('my-header .user-menu') || + document.querySelector('my-header .action-buttons') || + document.querySelector('.header-right') || + document.querySelector('header .right'); + + var token = localStorage.getItem('access_token'); + var uName = localStorage.getItem('username'); + var uEmail = localStorage.getItem('email'); + var displayName = localStorage.getItem('displayName') || uName || ''; + var emailStr = uEmail || ''; + var isAuth = !!token && !!uName; + + if (headerRight) { + var liveWidget = headerRight.querySelector('#yaknet-header-account-widget'); + if (!liveWidget) { + var widgetWrap = document.createElement('div'); + widgetWrap.id = 'yaknet-header-account-widget'; + widgetWrap.className = 'yaknet-header-widget'; + widgetWrap.style.display = 'inline-flex'; + widgetWrap.style.alignItems = 'center'; + widgetWrap.style.marginRight = '8px'; + + widgetWrap.innerHTML = + '' + + ''; + + var targetSibling = + headerRight.querySelector('my-user-menu') || + headerRight.querySelector('my-login-link') || + headerRight.querySelector('.user-menu') || + headerRight.querySelector('.settings-button') || + headerRight.querySelector('my-user-notifications') || + headerRight.querySelector('my-notification-bell'); + + if (targetSibling && targetSibling.parentElement === headerRight) { + headerRight.insertBefore(widgetWrap, targetSibling); + } else { + headerRight.prepend(widgetWrap); + } + + var defaultLoginLink = headerRight.querySelector('my-login-link'); + if (defaultLoginLink) { + defaultLoginLink.setAttribute('aria-hidden', 'true'); + defaultLoginLink.setAttribute('tabindex', '-1'); + defaultLoginLink.style.display = 'none'; + } + } else { + // Sync widget attributes dynamically if auth state changed + var el = liveWidget.querySelector('yaknet-account'); + if (el) { + if (el.shadowRoot) { + var shadowLogoutBtn = + el.shadowRoot.getElementById('logoutBtn') || + el.shadowRoot.querySelector('.yn-logout-btn') || + el.shadowRoot.querySelector('button[title*="Çıkış"], button[aria-label*="Çıkış"]'); + if (shadowLogoutBtn && !shadowLogoutBtn.hasAttribute('data-yaktube-hooked')) { + shadowLogoutBtn.setAttribute('data-yaktube-hooked', 'true'); + shadowLogoutBtn.addEventListener( + 'click', + function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + performUnifiedLogout(); + }, + true + ); + } + } + + var expectedAuth = isAuth ? 'true' : 'false'; + var expectedName = isAuth ? displayName : ''; + var expectedEmail = isAuth ? emailStr : ''; + if (el.getAttribute('authenticated') !== expectedAuth || el.getAttribute('user-name') !== expectedName) { + el.setAttribute('authenticated', expectedAuth); + el.setAttribute('user-name', expectedName); + el.setAttribute('user-email', expectedEmail); + if (typeof el.syncStateFromAttributes === 'function') { + el.syncStateFromAttributes(); + el.render(); + } + } + } + } + } + + // 2.2 Login Modal / Page + var loginCard = + document.querySelector('my-login') || + document.querySelector('.login-page') || + document.querySelector('.login-container') || + document.querySelector('form.login') || + document.querySelector('form[action*="login"]') || + document.querySelector('my-login-modal'); + + if (loginCard && !document.getElementById('yaknet-login-section')) { + var isLocalLogin = window.location.search.includes('local=true') || window.location.pathname.includes('/admin'); + + var ssoBox = document.createElement('div'); + ssoBox.id = 'yaknet-login-section'; + ssoBox.className = 'yaknet-login-banner'; + ssoBox.innerHTML = + '
YakNet ile YakTube\'a Katılın
' + + '

' + + 'YouTube gibi tek bir YakNet hesabıyla güvenle giriş yapabilir, kendi video ve müzik kanallarınızı oluşturabilirsiniz.' + + '

' + + '' + + '
' + + 'YakNet hesabınız yok mu? YakNet Hesabı Oluştur' + + '
' + + '
' + + 'Giriş yaparak Kullanım Koşulları\'nı ve Gizlilik Politikası\'nı kabul etmiş sayılırsınız.' + + '
' + + ''; + + var formFirstChild = loginCard.firstElementChild; + if (formFirstChild) { + loginCard.insertBefore(ssoBox, formFirstChild); + } else { + loginCard.appendChild(ssoBox); + } + + var registerLinks = document.querySelectorAll('a[href*="/signup"], a[href*="/register"]'); + registerLinks.forEach(function (l) { + l.href = 'https://auth.yakhub.com.tr/register'; + l.target = '_blank'; + l.innerText = 'YakNet Hesabı Oluştur'; + }); + + var formInputs = loginCard.querySelector('.form-group, .inputs, form') || loginCard.querySelector('input'); + var containerToHide = formInputs ? formInputs.closest('form') || formInputs.parentElement : null; + + if (containerToHide && !isLocalLogin) { + var subElements = Array.from(loginCard.children).filter(function (el) { + return ( + el.id !== 'yaknet-login-section' && !el.classList.contains('title') && !el.classList.contains('page-title') + ); + }); + + subElements.forEach(function (el) { + el.style.display = 'none'; + }); + + var showAdminBtn = document.getElementById('yaktube-show-admin-login'); + if (showAdminBtn) { + showAdminBtn.addEventListener('click', function (e) { + e.preventDefault(); + subElements.forEach(function (el) { + el.style.display = ''; + }); + showAdminBtn.style.display = 'none'; + announce('Yönetici kullanıcı adı ve şifre giriş formu açıldı.'); + }); + } + } + } + } + + // 3. Intelligent Branding & A11y DOM Auto-Fixer + function autoFixDOM() { + injectYakNetSSO(); + + // Aggressively remove generic Framasoft Open-in-App / Play Store banners + var genericAppBanners = document.querySelectorAll( + 'my-open-app, my-client-app-banner, .open-app-container, .app-banner, [class*="open-app"], [class*="open-in-app"], .mobile-app-banner, my-open-in-app, .app-promotion, a[href*="play.google.com/store/apps/details?id=app.joinpeertube"], a[href*="f-droid.org"]' + ); + genericAppBanners.forEach(function (banner) { + banner.setAttribute('aria-hidden', 'true'); + banner.setAttribute('tabindex', '-1'); + if (banner.parentElement) { + try { + banner.remove(); + } catch (e) { + banner.style.display = 'none'; + } + } + }); + + // Replace leftover PeerTube branding in visible UI texts + var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false); + var node; + while ((node = walker.nextNode())) { + var val = node.nodeValue; + if (val && (val.includes('PeerTube') || val.includes('Peertube'))) { + var parentTag = node.parentElement ? node.parentElement.tagName : ''; + var parentElem = node.parentElement; + if ( + parentElem && + (parentElem.closest('#yaktube-ondemand-container') || + parentElem.closest('#yaktube-live-watch-container') || + parentElem.tagName === 'SCRIPT' || + parentElem.tagName === 'STYLE') + ) { + continue; + } + if (true) { + node.nodeValue = val + .replace(/PeerTube hakkında/gi, 'YakTube Hakkında') + .replace(/PeerTube'a katılın/gi, "YakTube'a Katılın") + .replace(/PeerTube örneği/gi, 'YakTube Sunucusu') + .replace(/PeerTube/g, 'YakTube'); + } + } + } + + // Semantic Headings (H3 for video titles, H2 for sections) + document + .querySelectorAll('.video-miniature, my-video-miniature, .video-card, .yaktube-card, [class*="miniature"]') + .forEach(function (card) { + var titleElement = card.querySelector( + '.video-miniature-name, .video-name, .yaktube-video-name, a[title], .title' + ); + if (titleElement) { + if (!['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(titleElement.tagName)) { + titleElement.setAttribute('role', 'heading'); + titleElement.setAttribute('aria-level', '3'); + } + } + }); + + document + .querySelectorAll('.section-title, .sub-header, .videos-header, .page-header, .yaktube-title-h3') + .forEach(function (sec) { + if (!['H1', 'H2'].includes(sec.tagName)) { + sec.setAttribute('role', 'heading'); + sec.setAttribute('aria-level', '2'); + } + }); + + // Make native PeerTube comment authors accessible as H3 headings for screen readers + document + .querySelectorAll('my-video-comment, .video-comment, my-video-comment-thread, .comment, .comment-thread') + .forEach(function (commentEl) { + var authorEl = commentEl.querySelector( + '.display-name, .actor-name, .comment-author, a.actor-link, .video-comment-header-left a, .video-comment-header-left span' + ); + if ( + authorEl && + !['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(authorEl.tagName) && + !authorEl.getAttribute('role') + ) { + authorEl.setAttribute('role', 'heading'); + authorEl.setAttribute('aria-level', '3'); + } + }); + + // Auto-fix unlabeled buttons + document.querySelectorAll('button:not([data-a11y-fixed])').forEach(function (btn) { + btn.setAttribute('data-a11y-fixed', 'true'); + var text = (btn.textContent || '').trim(); + var hasAria = btn.getAttribute('aria-label') || btn.getAttribute('title'); + + if (!text && !hasAria) { + var html = btn.innerHTML.toLowerCase(); + var className = (btn.className || '').toLowerCase(); + var icon = btn.querySelector('my-global-icon, i, svg, [class*="icon-"]'); + var iconName = icon ? icon.className || icon.getAttribute('name') || '' : ''; + + if (html.includes('search') || className.includes('search') || iconName.includes('search')) { + btn.setAttribute('aria-label', 'Ara'); + } else if (html.includes('bell') || className.includes('notification') || iconName.includes('bell')) { + btn.setAttribute('aria-label', 'Bildirimler'); + } else if (html.includes('cog') || html.includes('setting') || className.includes('setting')) { + btn.setAttribute('aria-label', 'Ayarlar'); + } else if ( + html.includes('menu') || + html.includes('bars') || + className.includes('burger') || + className.includes('menu') + ) { + btn.setAttribute('aria-label', 'Ana Gezinme Menüsü'); + } else if (html.includes('play') || className.includes('play')) { + btn.setAttribute('aria-label', 'Oynat'); + } else if (html.includes('pause') || className.includes('pause')) { + btn.setAttribute('aria-label', 'Duraklat'); + } else if (html.includes('volume') || html.includes('mute') || className.includes('volume')) { + btn.setAttribute('aria-label', 'Ses Kontrolü'); + } else if (html.includes('fullscreen') || className.includes('fullscreen')) { + btn.setAttribute('aria-label', 'Tam Ekran'); + } else if ( + html.includes('ellipsis') || + html.includes('more') || + className.includes('more') || + className.includes('action') + ) { + btn.setAttribute('aria-label', 'Diğer İşlemler ve Seçenekler'); + } else if (html.includes('trash') || html.includes('delete') || className.includes('delete')) { + btn.setAttribute('aria-label', 'Sil'); + } else if (html.includes('edit') || className.includes('edit')) { + btn.setAttribute('aria-label', 'Düzenle'); + } else if (html.includes('share') || className.includes('share')) { + btn.setAttribute('aria-label', 'Paylaş'); + } else if (html.includes('thumb-up') || html.includes('like') || className.includes('like')) { + btn.setAttribute('aria-label', 'Beğen'); + } else if (html.includes('thumb-down') || html.includes('dislike') || className.includes('dislike')) { + btn.setAttribute('aria-label', 'Beğenme'); + } else if (html.includes('filter') || className.includes('filter')) { + btn.setAttribute('aria-label', 'Filtreler'); + } else if (html.includes('close') || html.includes('times') || className.includes('close')) { + btn.setAttribute('aria-label', 'Kapat'); + } else if (btn.closest('.vjs-control-bar')) { + btn.setAttribute('aria-label', 'Oynatıcı Düğmesi'); + } else { + var parent = btn.parentElement; + if (parent && parent.title) { + btn.setAttribute('aria-label', parent.title); + } else { + btn.setAttribute('aria-label', 'Düğme'); + } + } + } + }); + + // Auto-fix unlabeled images + document.querySelectorAll('img:not([data-a11y-fixed])').forEach(function (img) { + img.setAttribute('data-a11y-fixed', 'true'); + var alt = img.getAttribute('alt'); + if (!alt || alt.trim() === '') { + var card = img.closest('.video-miniature, .video-card, .yaktube-card, [class*="miniature"]'); + if (card) { + var titleEl = card.querySelector('.video-miniature-name, .video-name, .yaktube-video-name, h3, h4, a[title]'); + var titleText = titleEl ? (titleEl.textContent || titleEl.getAttribute('title') || '').trim() : ''; + if (titleText) { + img.setAttribute('alt', titleText + ' video kapağı'); + return; + } + } + + var avatarContainer = img.closest('.actor-avatar, .channel-avatar, .avatar, [class*="avatar"]'); + if (avatarContainer) { + var nameEl = avatarContainer.parentElement + ? avatarContainer.parentElement.querySelector('.name, .actor-name, .channel-name') + : null; + var nameText = nameEl ? (nameEl.textContent || '').trim() : ''; + img.setAttribute('alt', (nameText ? nameText + ' ' : '') + 'Profil Resmi'); + return; + } + + if (img.src && (img.src.includes('logo') || img.className.includes('logo'))) { + img.setAttribute('alt', getInstanceName() + ' Logo'); + return; + } + + var parentLink = img.closest('a'); + if (parentLink && parentLink.textContent && parentLink.textContent.trim().length > 0) { + img.setAttribute('alt', ''); + } else if (parentLink && parentLink.getAttribute('title')) { + img.setAttribute('alt', parentLink.getAttribute('title')); + } else { + img.setAttribute('alt', 'Görsel'); + } + } + }); + + // Auto-fix unlabeled links + document.querySelectorAll('a:not([data-a11y-fixed])').forEach(function (a) { + a.setAttribute('data-a11y-fixed', 'true'); + var text = (a.textContent || '').trim(); + var hasAria = a.getAttribute('aria-label') || a.getAttribute('title'); + if (!text && !hasAria) { + var img = a.querySelector('img'); + if (img && img.alt) { + a.setAttribute('aria-label', img.alt); + } else if (a.href) { + if (a.href.includes('/videos/watch/')) { + a.setAttribute('aria-label', 'Videoyu İzle'); + } else if (a.href.includes('/video-channels/')) { + a.setAttribute('aria-label', 'Kanal Sayfasına Git'); + } else if (a.href.includes('/accounts/')) { + a.setAttribute('aria-label', 'Hesap Profiline Git'); + } else if (a.href.includes('/videos/search')) { + a.setAttribute('aria-label', 'Arama Sayfası'); + } else { + a.setAttribute('aria-label', 'Bağlantı'); + } + } + } + }); + + // Auto-fix search inputs + document.querySelectorAll('input:not([data-a11y-fixed])').forEach(function (inp) { + inp.setAttribute('data-a11y-fixed', 'true'); + if (!inp.getAttribute('aria-label') && !inp.getAttribute('placeholder') && !(inp.labels && inp.labels.length)) { + var type = (inp.getAttribute('type') || 'text').toLowerCase(); + if (type === 'search') { + inp.setAttribute('aria-label', getInstanceName() + ' Video Arama'); + } else if (type === 'checkbox') { + inp.setAttribute('aria-label', 'Seçim Kutusu'); + } else if (type === 'password') { + inp.setAttribute('aria-label', 'Şifre'); + } + } + }); + } + + setInterval(autoFixDOM, 1000); + + // 3. Keyboard Shortcuts Modal + function toggleShortcutsModal() { + var modal = document.getElementById('yaktube-shortcuts-dialog'); + if (modal) { + if (modal.style.display === 'none' || !modal.style.display) { + modal.style.display = 'flex'; + announce('Klavye kısayolları yardım penceresi açıldı. Kapatmak i�in Escape tuşuna basın.'); + var closeBtn = document.getElementById('yaktube-shortcuts-close'); + if (closeBtn) closeBtn.focus(); + } else { + modal.style.display = 'none'; + announce('Yardım penceresi kapatıldı.'); + } + } else { + modal = document.createElement('div'); + modal.id = 'yaktube-shortcuts-dialog'; + modal.className = 'yaktube-modal-overlay'; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + modal.setAttribute('aria-labelledby', 'yaktube-shortcuts-title'); + modal.innerHTML = + '
' + + '

?? YakTube Klavye K�sayollar�

' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
Kısayolİşlev
Space veya KOynat / Duraklat
Sol Ok veya J5 saniye geri sar
Sağ Ok veya L5 saniye ileri sar
Yukarı / Aşağı OkSesi %5 artır / azalt
MSesi Kapat / A� (Mute)
FTam Ekran
CAltyazıları A� / Kapat
/ (Bölü)Arama kutusuna hızlı git
0 - 9Videonun %0 - %90 noktasına atla
? (Shift+/)Bu yardım kılavuzunu aç / kapat
EscPencereleri kapat
' + + '' + + '
'; + document.body.appendChild(modal); + + document.getElementById('yaktube-shortcuts-close').addEventListener('click', function () { + modal.style.display = 'none'; + }); + + var closeBtn = document.getElementById('yaktube-shortcuts-close'); + if (closeBtn) closeBtn.focus(); + announce('Klavye kısayolları yardım penceresi açıldı. Kapatmak i�in Escape tuşuna basın.'); + } + } + + function isInputFocused() { + var el = document.activeElement; + if (!el) return false; + var tag = el.tagName ? el.tagName.toUpperCase() : ''; + return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable; + } + + function getVideoElement() { + return document.querySelector('video'); + } + + function formatTime(seconds) { + if (isNaN(seconds) || seconds < 0) return '0 saniye'; + var mins = Math.floor(seconds / 60); + var secs = Math.floor(seconds % 60); + if (mins === 0) return secs + ' saniye'; + return mins + ' dakika ' + secs + ' saniye'; + } + + // 4. Global Keydown Listener + window.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + var shortcutsModal = document.getElementById('yaktube-shortcuts-dialog'); + if (shortcutsModal && shortcutsModal.style.display !== 'none') { + shortcutsModal.style.display = 'none'; + announce('Pencere kapatıldı.'); + return; + } + var importModal = document.getElementById('yaktube-import-modal'); + if (importModal && importModal.style.display !== 'none') { + importModal.style.display = 'none'; + announce('İndirme penceresi kapatıldı.'); + return; + } + if (isInputFocused()) { + document.activeElement.blur(); + } + return; + } + + if (e.key === '?' || (e.shiftKey && e.key === '/')) { + if (!isInputFocused()) { + e.preventDefault(); + toggleShortcutsModal(); + return; + } + } + + if (e.key === '/' && !isInputFocused()) { + e.preventDefault(); + var searchInput = + document.querySelector('input[type="search"]') || + document.querySelector('input[name="search"]') || + document.querySelector('.search-input') || + document.querySelector('input'); + if (searchInput) { + searchInput.focus(); + if (searchInput.select) searchInput.select(); + announce('Arama kutusuna odaklanıldı.'); + } + return; + } + + if (isInputFocused()) { + return; + } + + var video = getVideoElement(); + if (!video) return; + + var key = e.key.toLowerCase(); + + if (key === ' ' || key === 'k') { + e.preventDefault(); + if (video.paused) { + video.play(); + announce('Video oynatılıyor.'); + } else { + video.pause(); + announce('Video duraklatıldı.'); + } + } else if (key === 'j' || e.key === 'ArrowLeft') { + e.preventDefault(); + video.currentTime = Math.max(0, video.currentTime - 5); + announce('5 saniye geri sarıldı. Şu an: ' + formatTime(video.currentTime)); + } else if (key === 'l' || e.key === 'ArrowRight') { + e.preventDefault(); + video.currentTime = Math.min(video.duration || 99999, video.currentTime + 5); + announce('5 saniye ileri sarıldı. Şu an: ' + formatTime(video.currentTime)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + video.volume = Math.min(1, video.volume + 0.05); + announce('Ses seviyesi yüzde ' + Math.round(video.volume * 100)); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + video.volume = Math.max(0, video.volume - 0.05); + announce('Ses seviyesi yüzde ' + Math.round(video.volume * 100)); + } else if (key === 'm') { + e.preventDefault(); + video.muted = !video.muted; + announce(video.muted ? 'Ses kapatıldı (Sessiz).' : 'Ses açıldı.'); + } else if (key === 'f') { + e.preventDefault(); + if (!document.fullscreenElement) { + var container = video.closest('.video-js') || video.parentElement || video; + if (container.requestFullscreen) container.requestFullscreen(); + announce('Tam ekran moduna geçildi.'); + } else { + if (document.exitFullscreen) document.exitFullscreen(); + announce('Tam ekrandan çıkıldı.'); + } + } else if (key === 'c') { + e.preventDefault(); + var tracks = video.textTracks; + if (tracks && tracks.length > 0) { + var isShowing = tracks[0].mode === 'showing'; + tracks[0].mode = isShowing ? 'disabled' : 'showing'; + announce(isShowing ? 'Altyazılar kapatıldı.' : 'Altyazılar açıldı.'); + } else { + announce('Bu videoda altyazı bulunmuyor.'); + } + } else if (key >= '0' && key <= '9' && !e.ctrlKey && !e.altKey && !e.metaKey) { + e.preventDefault(); + var percent = parseInt(key, 10) * 10; + if (video.duration) { + video.currentTime = (video.duration * percent) / 100; + announce('Videonun yüzde ' + percent + ' noktasına atlandı. Şu an: ' + formatTime(video.currentTime)); + } + } + }); + + // 5. On-Demand Search & Import with Duplicate Awareness + var lastQuery = null; + var isSearching = false; + + function showModal(title, desc) { + var overlay = document.getElementById('yaktube-import-modal'); + if (!overlay) { + overlay = document.createElement('div'); + overlay.id = 'yaktube-import-modal'; + overlay.className = 'yaktube-modal-overlay'; + overlay.setAttribute('role', 'dialog'); + overlay.setAttribute('aria-modal', 'true'); + overlay.setAttribute('aria-labelledby', 'yaktube-modal-title'); + overlay.innerHTML = + '
' + + '' + + '

' + + (title || "YakTube'a Aktarılıyor...") + + '

' + + '

' + + (desc || 'Seçtiğiniz video D: diskinize aktarılıyor...') + + '

' + + '
'; + document.body.appendChild(overlay); + } else { + var titleElem = document.getElementById('yaktube-modal-title'); + var descElem = document.getElementById('yaktube-modal-desc'); + if (titleElem) titleElem.innerText = title; + if (descElem) descElem.innerText = desc; + overlay.style.display = 'flex'; + } + announce((title || '') + '. ' + (desc || ''), true); + } + + function hideModal() { + var overlay = document.getElementById('yaktube-import-modal'); + if (overlay) overlay.style.display = 'none'; + } + + function handleLiveStream(url, videoTitle, isRestoring) { + var videoIdMatch = url.match(/[?&]v=([^&]+)/) || url.match(/youtu\.be\/([^?]+)/); + var videoId = videoIdMatch ? videoIdMatch[1] : ''; + + if (!videoId) { + alert('Canl\u0131 yay\u0131n ID bulunamad\u0131.'); + return; + } + + // Persist active live stream state in sessionStorage + try { + sessionStorage.setItem( + 'yaktube_active_live', + JSON.stringify({ + url: url, + title: videoTitle, + videoId: videoId + }) + ); + } catch (e) {} + + var liveContainer = document.getElementById('yaktube-live-watch-container'); + if (liveContainer) liveContainer.remove(); + + liveContainer = document.createElement('div'); + liveContainer.id = 'yaktube-live-watch-container'; + liveContainer.className = 'yaktube-live-watch-section'; + liveContainer.setAttribute('role', 'region'); + liveContainer.setAttribute('tabindex', '-1'); + liveContainer.setAttribute('aria-label', videoTitle + ' Canl\u0131 Yay\u0131n\u0131'); + + liveContainer.innerHTML = + '
' + + '
' + + '
' + + '\ud83d\udd34 CANLI YAYIN' + + '

' + + videoTitle + + '

' + + '
' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '
\ud83d\udce1 YakTube Canl\u0131 Yay\u0131n Ak\u0131\u015f\u0131 \u2022 Reklams\u0131z ve Kesintisiz
' + + '
ESC tu\u015funa basarak yay\u0131n\u0131 kapatabilirsiniz.
' + + '
' + + '
' + + '
' + + '

' + + '\ud83d\udcac YouTube Topluluk Yorumlar\u0131' + + '

' + + '' + + '
' + + '' + + '' + + '
' + + '
'; + + var mainMount = + document.querySelector('my-search, my-search-results, router-outlet, main, .main-col') || document.body; + if (mainMount && mainMount.firstChild) { + mainMount.insertBefore(liveContainer, mainMount.firstChild); + } else if (mainMount) { + mainMount.appendChild(liveContainer); + } + + if (!isRestoring) { + liveContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); + setTimeout(function () { + var liveTitle = document.getElementById('yaktube-live-title'); + if (liveTitle) liveTitle.focus(); + else liveContainer.focus(); + }, 150); + announce( + videoTitle + + ' canl\u0131 yay\u0131n\u0131 ba\u015flat\u0131ld\u0131. Canl\u0131 yay\u0131n alan\u0131na odaklan\u0131ld\u0131.', + true + ); + } + + var commentNextToken = null; + var commentsList = document.getElementById('yaktube-comments-list'); + var toggleCommentsBtn = document.getElementById('yaktube-toggle-comments-btn'); + var moreCommentsWrap = document.getElementById('yaktube-more-comments-wrap'); + var moreCommentsBtn = document.getElementById('yaktube-load-more-comments-btn'); + + function renderCommentCard(c) { + var card = document.createElement('div'); + card.className = 'yaktube-comment-item'; + card.setAttribute('role', 'article'); + card.setAttribute('tabindex', '0'); + card.setAttribute('aria-label', c.author + ' isimli kullan\u0131c\u0131n\u0131n yorumu: ' + c.content); + card.style.cssText = + 'background: #1e293b; padding: 12px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); display: flex; gap: 12px; align-items: flex-start;'; + + var avatarLetter = (c.author || 'U').replace('@', '').charAt(0).toUpperCase(); + var avatarHtml = + '
' + + avatarLetter + + '
'; + + var safeContent = c.content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + card.innerHTML = + avatarHtml + + '
' + + '
' + + '

' + + c.author + + '

' + + '' + + c.published + + '' + + (c.likeCount && c.likeCount !== '0' + ? '👍 ' + + c.likeCount + + '' + : '') + + '
' + + '
' + + safeContent + + '
' + + '
'; + + return card; + } + + function fetchComments(token) { + if (!token) { + toggleCommentsBtn.disabled = true; + toggleCommentsBtn.textContent = '⏳ Yorumlar Y\u00fckleniyor...'; + } else { + moreCommentsBtn.disabled = true; + moreCommentsBtn.textContent = '⏳ Y\u00fckleniyor...'; + } + + var fetchUrl = + '/api-custom/youtube-comments?id=' + + encodeURIComponent(videoId) + + (token ? '&token=' + encodeURIComponent(token) : ''); + fetch(fetchUrl) + .then(function (r) { + return r.json(); + }) + .then(function (res) { + if (!token) { + commentsList.innerHTML = ''; + commentsList.style.display = 'flex'; + toggleCommentsBtn.textContent = '🔄 Yorumlar\u0131 Yenile'; + toggleCommentsBtn.disabled = false; + } else { + moreCommentsBtn.textContent = '➕ Daha Fazla Yorum G\u00f6ster (+20 Yorum)'; + moreCommentsBtn.disabled = false; + } + + var list = res.comments || []; + if (list.length === 0 && !token) { + commentsList.innerHTML = + '
Bu videoda/yay\u0131nda hen\u00fcz yorum bulunmuyor.
'; + moreCommentsWrap.style.display = 'none'; + announce('Bu videoda henüz yorum bulunmuyor.'); + return; + } + + list.forEach(function (c) { + commentsList.appendChild(renderCommentCard(c)); + }); + + commentNextToken = res.nextToken; + if (commentNextToken) { + moreCommentsWrap.style.display = 'block'; + } else { + moreCommentsWrap.style.display = 'none'; + } + + announce(list.length + ' adet YouTube yorumu yüklendi.'); + }) + .catch(function (err) { + if (!token) { + toggleCommentsBtn.disabled = false; + toggleCommentsBtn.textContent = '⚠️ Tekrar Dene'; + } else { + moreCommentsBtn.disabled = false; + moreCommentsBtn.textContent = '⚠️ Tekrar Dene'; + } + alert('Yorumlar yüklenirken hata oluştu: ' + err.message); + }); + } + + if (toggleCommentsBtn) { + toggleCommentsBtn.addEventListener('click', function () { + fetchComments(null); + }); + } + if (moreCommentsBtn) { + moreCommentsBtn.addEventListener('click', function () { + if (commentNextToken) fetchComments(commentNextToken); + }); + } + + function closeLiveTheater() { + try { + sessionStorage.removeItem('yaktube_active_live'); + } catch (e) {} + if (liveContainer && liveContainer.parentNode) { + var ifr = liveContainer.querySelector('iframe'); + if (ifr) ifr.src = ''; + liveContainer.remove(); + } + document.removeEventListener('keydown', handleEsc); + announce('Canl\u0131 yay\u0131n izleme alan\u0131 kapat\u0131ld\u0131.'); + } + + function handleEsc(e) { + if (e.key === 'Escape' || e.keyCode === 27) { + closeLiveTheater(); + } + } + + var closeBtn = document.getElementById('yaktube-close-live-theater'); + if (closeBtn) closeBtn.addEventListener('click', closeLiveTheater); + document.addEventListener('keydown', handleEsc); + } + + function restoreActiveLiveStream() { + try { + var saved = sessionStorage.getItem('yaktube_active_live'); + if (!saved) return; + var data = JSON.parse(saved); + if (data && data.url && data.title && !document.getElementById('yaktube-live-watch-container')) { + handleLiveStream(data.url, data.title, true); + } + } 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..." + ); + + 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(); + }) + .then(function (data) { + if (!data || !data.ok) { + throw new Error(data && data.error ? data.error : 'İçe aktarma başlatılamadı.'); + } + + 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 = "📥 YakTube'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 = "📥 YakTube'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 = "📥 YakTube'a Aktar & İzle"; + alert('İndirme hatası: ' + err.message); + announce('İndirme hatası: ' + err.message, true); + }); + } + + function checkAndInjectSearchResults() { + var url = new URL(window.location.href); + var searchParam = url.searchParams.get('search'); + + if (!window.location.pathname.includes('/search') || !searchParam || searchParam.trim().length < 2) { + var existing = document.getElementById('yaktube-ondemand-container'); + if (existing) existing.remove(); + lastQuery = null; + return; + } + + var query = searchParam.trim(); + if (query === lastQuery) return; + lastQuery = query; + + var container = document.getElementById('yaktube-ondemand-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'yaktube-ondemand-container'; + container.className = 'yaktube-ondemand-section'; + container.setAttribute('role', 'region'); + container.setAttribute('aria-label', 'Canl\u0131 YouTube Arama Sonu\u00e7lar\u0131'); + + var mainContent = + document.querySelector('my-search, my-search-results, router-outlet, main, .main-col') || document.body; + if (mainContent && mainContent.firstChild) { + mainContent.insertBefore(container, mainContent.firstChild); + } else if (mainContent) { + mainContent.appendChild(container); + } + } + + container.innerHTML = + '
' + + '
' + + '\u26a1 Canl\u0131 YouTube & Web Arama' + + '

"' + + query + + '" i\u00e7in YouTube Sonu\u00e7lar\u0131 Taran\u0131yor...

' + + '
' + + '' + + '
' + + '
' + + '' + + '
YouTube\'daki en pop\u00fcler e\u015fle\u015fmeler taran\u0131yor...
' + + '
'; + + var shortcutsBtn = document.getElementById('yaktube-open-shortcuts'); + if (shortcutsBtn) { + shortcutsBtn.addEventListener('click', toggleShortcutsModal); + } + + announce(query + ' i\u00e7in canl\u0131 YouTube aramas\u0131 ba\u015flat\u0131ld\u0131.'); + isSearching = true; + + fetch('/api-custom/youtube-search?q=' + encodeURIComponent(query)) + .then(function (res) { + return res.json(); + }) + .then(function (data) { + var rawResults = data.results || []; + var uniqueResults = []; + var seenIds = {}; + + rawResults.forEach(function (item) { + if (item && item.id && !seenIds[item.id] && item.title) { + seenIds[item.id] = true; + uniqueResults.push(item); + } + }); + + var count = uniqueResults.length; + if (count === 0) { + container.innerHTML = + '
' + + '
' + + '⚡ Canlı YouTube & Web Arama' + + '

"' + + query + + '" için YouTube\'da Sonuç Bulunamadı

' + + '
' + + '
'; + announce(query + " için YouTube\'da eşleşen video bulunamadı."); + } else { + var countText = count === 1 ? '1 Video Bulundu' : count + ' Video Bulundu'; + announce(query + ' araması için ' + count + ' video listelendi.'); + var visibleCount = Math.min(12, count); + + function render() { + var visibleResults = uniqueResults.slice(0, visibleCount); + var cardsHtml = ''; + visibleResults.forEach(function (item) { + var isLocal = !!item.isDownloaded; + var isLive = !!item.isLive; + + var statusBadge = ''; + if (isLive) { + statusBadge = + '🔴 CANLI YAYIN'; + } else if (isLocal) { + statusBadge = + '✅ ' + + getInstanceName() + + "'da Mevcut"; + } + + var actionButton = ''; + if (isLive) { + actionButton = + ''; + } else if (isLocal) { + actionButton = + '▶️ ' + + getInstanceName() + + "'dan İzle"; + } else { + actionButton = + '"; + } + + var durationText = isLive ? 'CANLI' : item.durationFormatted; + var cardLabel = + item.title + + (isLive ? ' (Canlı Yayın)' : isLocal ? ' (' + getInstanceName() + ' kütüphanenizde mevcut)' : '') + + ', ' + + item.channel + + ' kanalı, Süre: ' + + durationText; + cardsHtml += + '
' + + '
' + + '' +
+                item.title.replace(/' + + '' + + durationText + + '' + + '
' + + '
' + + '
' + + statusBadge + + '

' + + item.title + + '

' + + '
' + + item.channel + + '
' + + '
' + + actionButton + + '
' + + '
'; + }); + + var loadMoreHtml = ''; + if (visibleCount < count) { + loadMoreHtml = + '
' + + '' + + '
'; + } + + container.innerHTML = + '
' + + '
' + + '⚡ Canlı YouTube & Web Arama' + + '

"' + + query + + '" için ' + + countText + + '

' + + '
' + + '' + + '
' + + '
' + + cardsHtml + + '
' + + loadMoreHtml; + + var sBtn = document.getElementById('yaktube-open-shortcuts'); + if (sBtn) { + sBtn.addEventListener('click', toggleShortcutsModal); + } + + var loadMoreBtn = document.getElementById('yaktube-load-more-btn'); + if (loadMoreBtn) { + loadMoreBtn.addEventListener('click', function () { + visibleCount = Math.min(visibleCount + 6, count); + render(); + announce(visibleCount + ' adet video gösteriliyor.'); + }); + } + + container.querySelectorAll('.yaktube-live-btn').forEach(function (btn) { + btn.addEventListener('click', function (e) { + var liveUrl = e.currentTarget.getAttribute('data-url'); + var liveTitle = e.currentTarget.getAttribute('data-title'); + handleLiveStream(liveUrl, liveTitle); + }); + }); + + container.querySelectorAll('.yaktube-import-btn').forEach(function (btn) { + btn.addEventListener('click', function (e) { + var videoUrl = e.currentTarget.getAttribute('data-url'); + var videoTitle = e.currentTarget.getAttribute('data-title'); + handleImport(videoUrl, e.currentTarget, videoTitle); + }); + }); + } + + render(); + } + }) + .catch(function (err) { + console.error('[YakTube] Search fetch error:', err); + container.innerHTML = + '
' + + '
' + + '\u26a1 Canl\u0131 YouTube & Web Arama' + + '

Arama s\u0131ras\u0131nda bir ba\u011flant\u0131 sorunu olu\u015ftu.

' + + '
' + + '
'; + announce('Arama s\u0131ras\u0131nda bir ba\u011flant\u0131 sorunu olu\u015ftu.'); + }) + .finally(function () { + isSearching = false; + }); + } + + // 7.5. Reactive Hybrid Related Videos Engine (Watch Page) + var currentActiveVideoKey = null; + var isFetchingRelated = false; + var relatedShowAll = false; + var cachedRelatedResults = []; + var cachedRelatedQuery = ''; + + function renderRelatedCards(container, results, query) { + var count = results ? results.length : 0; + if (count === 0) { + container.style.display = 'none'; + return; + } + + container.style.display = 'block'; + var limit = relatedShowAll ? count : Math.min(count, 12); + var displayed = results.slice(0, limit); + + var cardsHtml = ''; + displayed.forEach(function (item) { + var isLocal = !!item.isDownloaded; + var statusBadge = isLocal + ? '✅ ' + + getInstanceName() + + "'da İzle" + : '⚡ Canlı Keşif'; + + var actionButton = isLocal + ? '▶️ Hemen İzle' + : ''; + + var cardLabel = + item.title + + (isLocal ? ' (' + getInstanceName() + ' kütüphanenizde mevcut)' : ' (Canlı YouTube keşfi)') + + ', ' + + item.channel + + ' kanalı, Süre: ' + + item.durationFormatted; + cardsHtml += + '
' + + '
' + + '' +
+        item.title.replace(/' + + '' + + item.durationFormatted + + '' + + '
' + + '
' + + '
' + + statusBadge + + '

' + + item.title + + '

' + + '
' + + item.channel + + '
' + + '
' + + actionButton + + '
' + + '
'; + }); + + var loadMoreBtn = ''; + if (count > 12) { + if (!relatedShowAll) { + loadMoreBtn = + ''; + } else { + loadMoreBtn = + ''; + } + } + + var headerCountText = count > 12 && !relatedShowAll ? displayed.length + ' / ' + count : count; + + container.innerHTML = + '
' + + '
' + + '🔥 ' + + getInstanceName() + + ' Akıllı Önerilenler' + + '

"' + + query + + '" ile İlgili Öneriler (' + + headerCountText + + ')

' + + '
' + + '' + + '
' + + '
' + + cardsHtml + + '
' + + loadMoreBtn; + + var toggleBtn = document.getElementById('yaktube-related-toggle-more'); + if (toggleBtn) { + toggleBtn.addEventListener('click', function () { + relatedShowAll = !relatedShowAll; + renderRelatedCards(container, cachedRelatedResults, cachedRelatedQuery); + announce( + relatedShowAll ? 'Tüm ' + cachedRelatedResults.length + ' önerilen video açıldı.' : 'Öneriler daraltıldı.' + ); + }); + } + + var refreshBtn = document.getElementById('yaktube-related-refresh-btn'); + if (refreshBtn) { + refreshBtn.addEventListener('click', function () { + currentActiveVideoKey = null; + checkAndInjectRelatedVideos(true); + }); + } + + container.querySelectorAll('.yaktube-related-import').forEach(function (btn) { + btn.addEventListener('click', function (e) { + var videoUrl = e.currentTarget.getAttribute('data-url'); + var videoTitle = e.currentTarget.getAttribute('data-title'); + handleImport(videoUrl, e.currentTarget, videoTitle); + }); + }); + + container.querySelectorAll('.yaktube-related-play-link').forEach(function (link) { + link.addEventListener('click', function () { + currentActiveVideoKey = null; + }); + }); + } + + function checkAndInjectRelatedVideos(forceReload) { + var isWatchPage = window.location.pathname.includes('/videos/watch/') || window.location.pathname.startsWith('/w/'); + if (!isWatchPage) { + var existing = document.getElementById('yaktube-hybrid-related-container'); + if (existing) existing.remove(); + currentActiveVideoKey = null; + return; + } + + var titleElem = + document.querySelector('h1.video-name') || + document.querySelector('.video-info h1') || + document.querySelector('.video-watch-heading h1') || + document.querySelector('h1') || + document.querySelector('.title'); + + var rawTitle = titleElem ? (titleElem.textContent || titleElem.innerText || '').trim() : ''; + if (!rawTitle || rawTitle.length < 3) { + return; + } + + var currentPath = window.location.pathname; + var thisVideoKey = currentPath + '::' + rawTitle.toLowerCase(); + + if ( + !forceReload && + thisVideoKey === currentActiveVideoKey && + document.getElementById('yaktube-hybrid-related-container') + ) { + return; + } + + if (isFetchingRelated) return; + isFetchingRelated = true; + + var cleanTopic = rawTitle + .replace(/YakTube/gi, '') + .replace(/[\(\[].*?[\)\]]/g, '') + .replace(/official video|video klip|klip|hd|4k|lyric video|audio|remix|feat\.?|ft\.?/gi, '') + .trim(); + + var words = cleanTopic.split(/\s+/).filter(function (w) { + return w.length > 2; + }); + var searchQuery = words.slice(0, 3).join(' ') || cleanTopic; + + var container = document.getElementById('yaktube-hybrid-related-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'yaktube-hybrid-related-container'; + container.className = 'yaktube-ondemand-section'; + container.setAttribute('role', 'region'); + container.setAttribute('aria-label', 'YakTube Akıllı Önerilen Videolar'); + } + + var recVideosMount = document.querySelector('my-recommended-videos, .other-videos'); + var commentsMount = document.querySelector('#yaktube-watch-comments-panel, my-video-comments, .video-comments'); + var videoBottomMount = document.querySelector('.video-bottom, .video-info'); + var watchMount = document.querySelector('my-video-watch') || document.querySelector('main'); + + if (recVideosMount) { + if (container.parentElement !== recVideosMount) { + recVideosMount.prepend(container); + } + } else if (commentsMount) { + if (container.previousElementSibling !== commentsMount) { + commentsMount.insertAdjacentElement('afterend', container); + } + } else if (videoBottomMount) { + if (container.previousElementSibling !== videoBottomMount) { + videoBottomMount.insertAdjacentElement('afterend', container); + } + } else if (watchMount) { + if (container.parentElement !== watchMount) { + watchMount.appendChild(container); + } + } else { + isFetchingRelated = false; + return; + } + + container.innerHTML = + '
' + + '
' + + '🔥 ' + + getInstanceName() + + ' Akıllı Önerilenler' + + '

"' + + searchQuery + + '" için Öneriler Taranıyor...

' + + '
' + + '
' + + '
' + + '' + + '
YouTube ve Fediverse\'deki en alakalı videolar getiriliyor...
' + + '
'; + + announce(searchQuery + ' için ilgili video önerileri yükleniyor.'); + + fetch('/api-custom/youtube-search?q=' + encodeURIComponent(searchQuery)) + .then(function (res) { + return res.json(); + }) + .then(function (data) { + var rawResults = data.results || []; + var normalizedCurrentTitle = rawTitle.toLowerCase().trim(); + + var uniqueResults = []; + var seenIds = {}; + + rawResults.forEach(function (item) { + if (item && item.id && !seenIds[item.id] && item.title) { + var itemTitle = item.title.toLowerCase().trim(); + if (itemTitle !== normalizedCurrentTitle && !itemTitle.includes(normalizedCurrentTitle)) { + seenIds[item.id] = true; + uniqueResults.push(item); + } + } + }); + + cachedRelatedResults = uniqueResults; + cachedRelatedQuery = searchQuery; + currentActiveVideoKey = thisVideoKey; + relatedShowAll = false; + + renderRelatedCards(container, uniqueResults, searchQuery); + + var initialCount = Math.min(uniqueResults.length, 12); + if (uniqueResults.length === 0) { + announce(searchQuery + ' için ilgili öneri bulunamadı.'); + } else { + announce( + searchQuery + + ' için ' + + initialCount + + ' öneri gösteriliyor' + + (uniqueResults.length > 12 ? ', toplam ' + uniqueResults.length + ' video bulundu' : '') + + '.' + ); + } + }) + .catch(function (err) { + console.error('[YakTube] Related fetch error:', err); + container.style.display = 'none'; + }) + .finally(function () { + isFetchingRelated = false; + }); + } + + var lastWatchUuid = null; + + function checkAndInjectWatchPageComments() { + var pathname = window.location.pathname; + var match = pathname.match(/\/videos\/watch\/([a-zA-Z0-9_-]+)/) || pathname.match(/\/w\/([a-zA-Z0-9_-]+)/); + if (!match) { + lastWatchUuid = null; + var existingWatchComments = document.getElementById('yaktube-watch-comments-panel'); + if (existingWatchComments) existingWatchComments.remove(); + return; + } + + var uuid = match[1]; + var existingPanel = document.getElementById('yaktube-watch-comments-panel'); + if (uuid === lastWatchUuid && existingPanel) { + var nativeComments = document.querySelector('my-video-comments, .video-comments, .comments-block, #comments'); + if (nativeComments && existingPanel.previousElementSibling !== nativeComments) { + nativeComments.insertAdjacentElement('afterend', existingPanel); + } + return; + } + lastWatchUuid = uuid; + + var mountTarget = + document.querySelector('my-video-comments, .video-comments, my-video-watch, .video-bottom') || document.body; + if (!mountTarget) return; + + fetch('/api-custom/video-origin/' + encodeURIComponent(uuid)) + .then(function (r) { + return r.json(); + }) + .then(function (originData) { + if (!originData || !originData.ok || !originData.hasYouTubeOrigin || !originData.youtubeId) { + var old = document.getElementById('yaktube-watch-comments-panel'); + if (old) old.remove(); + return; + } + + var youtubeId = originData.youtubeId; + var existing = document.getElementById('yaktube-watch-comments-panel'); + if (existing) existing.remove(); + + var panel = document.createElement('div'); + panel.id = 'yaktube-watch-comments-panel'; + panel.className = 'yaktube-comments-panel'; + panel.setAttribute('role', 'region'); + panel.setAttribute('aria-label', 'YouTube Orijinal Yorumlar\u0131'); + panel.style.cssText = + 'margin-top: 24px; margin-bottom: 24px; background: #0f172a; border-radius: 12px; padding: 20px; border: 1px solid rgba(255,255,255,0.08); box-shadow: 0 8px 24px rgba(0,0,0,0.4);'; + + panel.innerHTML = + '
' + + '
' + + '

' + + "\ud83d\udcac YouTube'daki Orijinal Yorumlar" + + '

' + + '
Bu video YouTube\'dan aktar\u0131lm\u0131\u015ft\u0131r. Orijinal topluluk yorumlar\u0131n\u0131 a\u015fa\u011f\u0131dan anl\u0131k olarak y\u00fckleyebilirsiniz.
' + + '
' + + '' + + '
' + + '' + + ''; + + var nativeComments = document.querySelector('my-video-comments, .video-comments, .comments-block, #comments'); + if (nativeComments && nativeComments.parentElement) { + nativeComments.insertAdjacentElement('afterend', panel); + } else { + var watchCol = document.querySelector('.video-info, .video-bottom, my-video-watch, .main-col'); + if (watchCol) { + watchCol.appendChild(panel); + } else { + mountTarget.appendChild(panel); + } + } + + var watchCommentToken = null; + var listContainer = document.getElementById('yaktube-watch-comments-list'); + var toggleBtn = document.getElementById('yaktube-watch-toggle-comments-btn'); + var moreWrap = document.getElementById('yaktube-watch-more-comments-wrap'); + var moreBtn = document.getElementById('yaktube-watch-more-comments-btn'); + + function fetchWatchComments(token) { + if (!token) { + toggleBtn.disabled = true; + toggleBtn.textContent = '⏳ Yorumlar Y\u00fckleniyor...'; + } else { + moreBtn.disabled = true; + moreBtn.textContent = '⏳ Y\u00fckleniyor...'; + } + + var fUrl = + '/api-custom/youtube-comments?id=' + + encodeURIComponent(youtubeId) + + (token ? '&token=' + encodeURIComponent(token) : ''); + fetch(fUrl) + .then(function (r) { + return r.json(); + }) + .then(function (res) { + if (!token) { + listContainer.innerHTML = ''; + listContainer.style.display = 'flex'; + toggleBtn.textContent = '🔄 Yorumlar\u0131 Yenile'; + toggleBtn.disabled = false; + } else { + moreBtn.textContent = '➕ Daha Fazla Yorum G\u00f6ster (+20 Yorum)'; + moreBtn.disabled = false; + } + + var cList = res.comments || []; + if (cList.length === 0 && !token) { + listContainer.innerHTML = + '
Bu videoda hen\u00fcz YouTube yorumu bulunmuyor.
'; + moreWrap.style.display = 'none'; + announce('Bu videoda henüz YouTube yorumu bulunmuyor.'); + return; + } + + cList.forEach(function (c) { + var card = document.createElement('div'); + card.className = 'yaktube-comment-item'; + card.setAttribute('role', 'article'); + card.setAttribute('tabindex', '0'); + card.setAttribute('aria-label', c.author + ' isimli kullan\u0131c\u0131n\u0131n yorumu: ' + c.content); + card.style.cssText = + 'background: #1e293b; padding: 14px 16px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); display: flex; gap: 12px; align-items: flex-start;'; + + var avatarLetter = (c.author || 'U').replace('@', '').charAt(0).toUpperCase(); + var avatarHtml = + '
' + + avatarLetter + + '
'; + var safeContent = c.content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + + card.innerHTML = + avatarHtml + + '
' + + '
' + + '

' + + c.author + + '

' + + '' + + c.published + + '' + + (c.likeCount && c.likeCount !== '0' + ? '👍 ' + + c.likeCount + + '' + : '') + + '
' + + '
' + + safeContent + + '
' + + '
'; + + listContainer.appendChild(card); + }); + + watchCommentToken = res.nextToken; + if (watchCommentToken) { + moreWrap.style.display = 'block'; + } else { + moreWrap.style.display = 'none'; + } + + announce(cList.length + ' adet YouTube yorumu y\u00fcklendi.'); + }) + .catch(function (err) { + if (!token) { + toggleBtn.disabled = false; + toggleBtn.textContent = '⚠️ Tekrar Dene'; + } else { + moreBtn.disabled = false; + moreBtn.textContent = '⚠️ Tekrar Dene'; + } + alert('Yorumlar y\u00fcklenirken hata olu\u015ftu: ' + err.message); + }); + } + + if (toggleBtn) { + toggleBtn.addEventListener('click', function () { + fetchWatchComments(null); + }); + } + if (moreBtn) { + moreBtn.addEventListener('click', function () { + if (watchCommentToken) fetchWatchComments(watchCommentToken); + }); + } + }) + .catch(function () {}); + } + + // 6. PWA Mobile App Install Engine & Standalone Detection + var isInsideYakTubeApp = + navigator.userAgent.indexOf('YakTubeMobileApp') !== -1 || + window.matchMedia('(display-mode: standalone)').matches || + window.navigator.standalone === true; + + var isMobileDevice = /Android|iPhone|iPad|iPod|Mobile|Tablet/i.test(navigator.userAgent); + + var deferredInstallPrompt = null; + + window.addEventListener('beforeinstallprompt', function (e) { + if (isInsideYakTubeApp) return; + e.preventDefault(); + deferredInstallPrompt = e; + injectPWAInstallButton(); + }); + + function injectPWAInstallButton() { + if (isInsideYakTubeApp) { + var existBtn = document.getElementById('yaktube-pwa-install-btn'); + if (existBtn) { + existBtn.setAttribute('aria-hidden', 'true'); + existBtn.remove(); + } + return; + } + if (!deferredInstallPrompt || document.getElementById('yaktube-pwa-install-btn')) return; + + var headerButtons = + document.querySelector('my-header .buttons-container') || + document.querySelector('.buttons-container') || + document.querySelector('my-header'); + if (!headerButtons) return; + + var installBtn = document.createElement('button'); + installBtn.id = 'yaktube-pwa-install-btn'; + installBtn.className = 'yaktube-pwa-btn'; + installBtn.setAttribute('type', 'button'); + installBtn.setAttribute('aria-label', 'YakTube Mobil Uygulamasını Yükle'); + installBtn.style.cssText = + 'background: linear-gradient(135deg, #ff8f37, #f97316); color: #000; border: none; padding: 6px 14px; border-radius: 20px; font-weight: 700; font-size: 12px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; margin-right: 8px; box-shadow: 0 4px 12px rgba(255,143,55,0.4);'; + installBtn.innerHTML = '📲 Uygulamayı Yükle'; + + installBtn.addEventListener('click', function () { + if (deferredInstallPrompt) { + deferredInstallPrompt.prompt(); + deferredInstallPrompt.userChoice.then(function (choiceResult) { + if (choiceResult.outcome === 'accepted') { + installBtn.remove(); + announce('YakTube uygulaması cihazınıza yükleniyor.'); + } + deferredInstallPrompt = null; + }); + } + }); + + headerButtons.prepend(installBtn); + } + + function injectApkDownloadButton() { + // 1. If inside APK: NEVER show APK button + if (isInsideYakTubeApp) { + var existBtnApp = document.getElementById('yaktube-apk-download-btn'); + if (existBtnApp) { + existBtnApp.setAttribute('aria-hidden', 'true'); + existBtnApp.remove(); + } + return; + } + + // 2. If on Desktop: NEVER show in top header (keep header clean) + if (!isMobileDevice) { + var existBtnDesktop = document.getElementById('yaktube-apk-download-btn'); + if (existBtnDesktop) { + existBtnDesktop.setAttribute('aria-hidden', 'true'); + existBtnDesktop.remove(); + } + return; + } + + // 3. If on Mobile Web: Show in header + if (document.getElementById('yaktube-apk-download-btn')) return; + + var headerButtons = + document.querySelector('my-header .buttons-container') || + document.querySelector('my-header .header-right') || + document.querySelector('.buttons-container') || + document.querySelector('my-header'); + if (!headerButtons) return; + + var apkBtn = document.createElement('a'); + 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('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);'; + apkBtn.innerHTML = '🤖 APK İndir (4.5 MB)'; + + headerButtons.prepend(apkBtn); + } + + function injectSidebarApkLink() { + // If inside APK: NEVER show in sidebar + if (isInsideYakTubeApp) { + var existLink = document.getElementById('yaktube-sidebar-apk-link'); + if (existLink) { + existLink.setAttribute('aria-hidden', 'true'); + existLink.remove(); + } + return; + } + + if (document.getElementById('yaktube-sidebar-apk-link')) return; + + var menuContainer = document.querySelector('my-menu, .menu, .main-menu, .nav-list'); + if (!menuContainer) return; + + var sidebarLink = document.createElement('a'); + 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('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;'; + sidebarLink.innerHTML = '🤖 YakTube Android APK (4.5 MB)'; + + menuContainer.appendChild(sidebarLink); + } + + setInterval(function () { + checkAndInjectSearchResults(); + checkAndInjectRelatedVideos(false); + restoreActiveLiveStream(); + checkAndInjectWatchPageComments(); + injectPWAInstallButton(); + injectApkDownloadButton(); + injectSidebarApkLink(); + }, 800); + restoreActiveLiveStream(); + window.addEventListener('popstate', function () { + currentActiveVideoKey = null; + checkAndInjectSearchResults(); + checkAndInjectRelatedVideos(true); + checkAndInjectWatchPageComments(); + }); + checkAndInjectWatchPageComments(); +})(); diff --git a/main.js b/main.js new file mode 100644 index 0000000..0fd7a2d --- /dev/null +++ b/main.js @@ -0,0 +1,267 @@ +const { execFile } = require('child_process'); +const path = require('path'); +const http = require('http'); + +const searchCache = new Map(); +const CACHE_TTL = 30 * 60 * 1000; // 30 minutes + +function getCached(q) { + const item = searchCache.get(q); + if (item && Date.now() - item.time < CACHE_TTL) { + return item.data; + } + return null; +} + +function setCache(q, data) { + searchCache.set(q, { time: Date.now(), data }); + if (searchCache.size > 200) { + const firstKey = searchCache.keys().next().value; + searchCache.delete(firstKey); + } +} + +let cachedAdminToken = null; +let tokenExpiresAt = 0; + +async function getAdminToken() { + if (cachedAdminToken && Date.now() < tokenExpiresAt) { + return cachedAdminToken; + } + + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: '127.0.0.1', + port: 9000, + path: '/api/v1/oauth-clients/local', + method: 'GET', + headers: { Host: 'yaktube.yakhub.com.tr' } + }, + res => { + let body = ''; + res.on('data', chunk => (body += chunk)); + res.on('end', () => { + try { + const oauth = JSON.parse(body); + const postData = new URLSearchParams({ + client_id: oauth.client_id, + client_secret: oauth.client_secret, + grant_type: 'password', + username: 'yaktube', + password: 'YakTubeAdmin2026!' + }).toString(); + + const tokenReq = http.request( + { + hostname: '127.0.0.1', + port: 9000, + path: '/api/v1/users/token', + method: 'POST', + headers: { + Host: 'yaktube.yakhub.com.tr', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }, + tokenRes => { + let tokenBody = ''; + tokenRes.on('data', c => (tokenBody += c)); + tokenRes.on('end', () => { + try { + const tData = JSON.parse(tokenBody); + if (tData.access_token) { + cachedAdminToken = tData.access_token; + tokenExpiresAt = Date.now() + 3600 * 1000; + resolve(cachedAdminToken); + } else { + reject(new Error('No access_token: ' + tokenBody)); + } + } catch (e) { + reject(e); + } + }); + } + ); + tokenReq.on('error', reject); + tokenReq.write(postData); + tokenReq.end(); + } catch (e) { + reject(e); + } + }); + } + ); + req.on('error', reject); + req.end(); + }); +} + +async function register({ getRouter, peertubeHelpers, logger }) { + const router = getRouter(); + const ytdlpPath = 'D:\\yaktube_storage\\bin\\yt-dlp.exe'; + + router.get('/search', async (req, res) => { + const query = (req.query.q || '').trim(); + if (!query || query.length < 2) { + return res.json({ results: [] }); + } + + const cached = getCached(query); + if (cached) { + return res.json({ results: cached, cached: true }); + } + + const args = ['ytsearch6:' + query, '--dump-single-json', '--flat-playlist', '--skip-download', '--no-warnings']; + + execFile(ytdlpPath, args, { maxBuffer: 10 * 1024 * 1024, timeout: 15000 }, (error, stdout, stderr) => { + if (error) { + if (logger) logger.error('YouTube search error: ' + error.message); + return res.status(500).json({ error: 'Search failed', details: error.message }); + } + + try { + const json = JSON.parse(stdout); + const entries = json.entries || []; + const results = entries + .filter(e => e && e.id) + .map(e => { + const duration = e.duration || 0; + const mins = Math.floor(duration / 60); + const secs = Math.floor(duration % 60); + const durationFormatted = (mins < 10 ? '0' + mins : mins) + ':' + (secs < 10 ? '0' + secs : secs); + let thumbnail = + e.thumbnail || (e.thumbnails && e.thumbnails.length ? e.thumbnails[e.thumbnails.length - 1].url : ''); + if (!thumbnail && e.id) { + thumbnail = `https://i.ytimg.com/vi/${e.id}/hqdefault.jpg`; + } + + return { + id: e.id, + title: e.title, + channel: e.uploader || e.channel || 'YouTube', + duration, + durationFormatted, + url: e.url || `https://www.youtube.com/watch?v=${e.id}`, + thumbnail + }; + }); + + setCache(query, results); + return res.json({ results, cached: false }); + } catch (parseErr) { + if (logger) logger.error('JSON parse error in search: ' + parseErr.message); + return res.status(500).json({ error: 'Failed to parse search results' }); + } + }); + }); + + router.post('/import', async (req, res) => { + const { targetUrl } = req.body; + if (!targetUrl || (!targetUrl.includes('youtube.com') && !targetUrl.includes('youtu.be'))) { + return res.status(400).json({ error: 'Invalid YouTube URL' }); + } + + try { + const token = await getAdminToken(); + const postData = new URLSearchParams({ + targetUrl: targetUrl, + channelId: '1146', // Türkçe Müzik channel + privacy: '1' + }).toString(); + + const importReq = http.request( + { + hostname: '127.0.0.1', + port: 9000, + path: '/api/v1/videos/imports', + method: 'POST', + headers: { + Host: 'yaktube.yakhub.com.tr', + Authorization: 'Bearer ' + token, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }, + importRes => { + let body = ''; + importRes.on('data', c => (body += c)); + importRes.on('end', () => { + try { + const data = JSON.parse(body); + if (data && data.video) { + return res.json({ + ok: true, + importId: data.id, + video: { + id: data.video.id, + uuid: data.video.uuid, + shortUUID: data.video.shortUUID, + name: data.video.name, + url: data.video.url + } + }); + } else { + return res.status(400).json({ error: 'Import initiation failed', details: data }); + } + } catch (e) { + return res.status(500).json({ error: 'Error parsing import response', raw: body }); + } + }); + } + ); + + importReq.on('error', err => { + return res.status(500).json({ error: 'HTTP error connecting to PeerTube API', details: err.message }); + }); + + importReq.write(postData); + importReq.end(); + } catch (err) { + return res.status(500).json({ error: 'Admin authentication failed', details: err.message }); + } + }); + + router.get('/status/:id', async (req, res) => { + const videoId = req.params.id; + const reqVideo = http.request( + { + hostname: '127.0.0.1', + port: 9000, + path: '/api/v1/videos/' + encodeURIComponent(videoId), + method: 'GET', + headers: { Host: 'yaktube.yakhub.com.tr' } + }, + resVideo => { + let body = ''; + resVideo.on('data', c => (body += c)); + resVideo.on('end', () => { + try { + const video = JSON.parse(body); + return res.json({ + id: video.id, + uuid: video.uuid, + shortUUID: video.shortUUID, + name: video.name, + state: video.state, + url: video.url, + isLocal: video.isLocal + }); + } catch (e) { + return res.status(500).json({ error: 'Failed to parse video info' }); + } + }); + } + ); + + reqVideo.on('error', e => res.status(500).json({ error: e.message })); + reqVideo.end(); + }); +} + +async function unregister() {} + +module.exports = { + register, + unregister +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c8cc37 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "peertube-plugin-yaktube-ondemand", + "version": "1.0.0", + "description": "Accessible On-Demand YouTube Import, Live Streams, Smart Recommendations and A11y Engine for PeerTube", + "main": "main.js", + "clientScripts": [ + { + "scopes": [ + "common" + ], + "script": "client.js" + } + ], + "css": [ + "style.css" + ], + "engine": { + "peertube": ">=5.0.0" + }, + "keywords": [ + "peertube", + "peertube-plugin", + "ondemand", + "youtube", + "yt-dlp", + "recommendations", + "accessibility", + "a11y", + "screen-reader", + "fediverse" + ], + "author": "Enes Yakıştır ", + "repository": { + "type": "git", + "url": "https://github.com/enesyakistir/peertube-plugin-yaktube-ondemand.git" + }, + "homepage": "https://yaktube.yakhub.com.tr", + "license": "AGPL-3.0-or-later" +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..544a009 --- /dev/null +++ b/style.css @@ -0,0 +1,407 @@ +/* YakTube Accessibility & On-Demand CSS */ + +:focus-visible { + outline: 3px solid #ff8f37 !important; + outline-offset: 3px !important; + box-shadow: 0 0 12px rgba(255, 143, 55, 0.7) !important; +} + +/* Screen reader only utility */ +.yaktube-sr-only { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} + +/* On-Demand Search Section */ +.yaktube-ondemand-section { + margin: 24px 0; + padding: 20px; + background: rgba(22, 22, 30, 0.92); + border: 1px solid rgba(255, 143, 55, 0.35); + border-radius: 16px; + backdrop-filter: blur(10px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); + color: #fff; + font-family: inherit; +} + +.yaktube-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + padding-bottom: 12px; +} + +.yaktube-header-left { + display: flex; + align-items: center; + gap: 12px; +} + +.yaktube-badge { + background: linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%); + color: #fff; + font-size: 11px; + font-weight: 700; + padding: 4px 10px; + border-radius: 20px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.yaktube-title-h3 { + margin: 0; + font-size: 1.15rem; + font-weight: 600; + color: #f1f1f1; +} + +.yaktube-shortcuts-btn { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + color: #ddd; + padding: 6px 12px; + border-radius: 8px; + font-size: 12px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; +} + +.yaktube-shortcuts-btn:hover { + background: rgba(255, 143, 55, 0.2); + border-color: #ff8f37; + color: #fff; +} + +.yaktube-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 18px; +} + +.yaktube-card { + background: #12121a; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + transition: + transform 0.2s ease, + border-color 0.2s ease, + box-shadow 0.2s ease; +} + +.yaktube-card:hover, +.yaktube-card:focus-within { + transform: translateY(-4px); + border-color: rgba(255, 143, 55, 0.6); + box-shadow: 0 10px 22px rgba(255, 143, 55, 0.18); +} + +.yaktube-thumb-wrap { + position: relative; + width: 100%; + padding-top: 56.25%; + background: #000; + overflow: hidden; +} + +.yaktube-thumb-img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.yaktube-card:hover .yaktube-thumb-img { + transform: scale(1.05); +} + +.yaktube-duration-badge { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.85); + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.15); +} + +.yaktube-info { + padding: 12px; + display: flex; + flex-direction: column; + justify-content: space-between; + flex-grow: 1; + gap: 10px; +} + +.yaktube-video-name { + font-size: 13px; + font-weight: 600; + line-height: 1.35; + color: #fff; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.yaktube-channel-name { + font-size: 11px; + color: #aaa; + margin-top: 4px; +} + +.yaktube-import-btn { + background: linear-gradient(135deg, #ff8f37 0%, #ff6600 100%); + border: none; + color: #fff; + padding: 8px 12px; + font-size: 12px; + font-weight: 600; + border-radius: 8px; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: + opacity 0.2s ease, + transform 0.1s ease; +} + +.yaktube-import-btn:hover { + opacity: 0.92; + transform: translateY(-1px); +} + +.yaktube-import-btn:disabled { + background: #555; + cursor: not-allowed; + opacity: 0.6; +} + +/* Modals */ +.yaktube-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(6px); + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; +} + +.yaktube-modal-card { + background: #181824; + border: 1px solid rgba(255, 143, 55, 0.5); + border-radius: 16px; + padding: 28px 24px; + max-width: 460px; + width: 90%; + text-align: center; + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.6); + color: #fff; +} + +.yaktube-modal-title { + font-size: 1.25rem; + font-weight: 700; + margin-top: 14px; + color: #ff8f37; +} + +.yaktube-modal-desc { + font-size: 13px; + color: #bbb; + margin-top: 8px; + line-height: 1.4; +} + +.yaktube-spinner { + width: 44px; + height: 44px; + border: 4px solid rgba(255, 143, 55, 0.2); + border-top-color: #ff8f37; + border-radius: 50%; + animation: yaktube-spin 0.8s linear infinite; + margin: 0 auto; +} + +@keyframes yaktube-spin { + to { + transform: rotate(360deg); + } +} + +/* Accessible Shortcuts Table */ +.yaktube-shortcuts-table { + width: 100%; + text-align: left; + border-collapse: collapse; + margin-top: 16px; + font-size: 13px; +} + +.yaktube-shortcuts-table th { + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + padding: 8px; + color: #ff8f37; +} + +.yaktube-shortcuts-table td { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + padding: 8px; + color: #ddd; +} + +.yaktube-kbd { + background: #2a2a38; + border: 1px solid #444; + border-radius: 4px; + padding: 2px 6px; + font-family: monospace; + font-weight: bold; + color: #ff8f37; +} + +.yaktube-modal-close-btn { + background: #ff8f37; + color: #111; + border: none; + padding: 8px 20px; + font-weight: 700; + border-radius: 8px; + cursor: pointer; + margin-top: 18px; +} +.yaktube-play-btn { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + color: #fff; + padding: 8px 12px; + font-size: 12px; + font-weight: 700; + border-radius: 8px; + cursor: pointer; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + text-decoration: none; + border: none; + box-sizing: border-box; + transition: + opacity 0.2s ease, + transform 0.1s ease; +} + +.yaktube-play-btn:hover { + opacity: 0.92; + transform: translateY(-1px); + color: #fff; +} +/* YakNet SSO Button and Account Header */ +.yaknet-login-banner { + background: linear-gradient(135deg, rgba(255, 143, 55, 0.15) 0%, rgba(255, 94, 58, 0.15) 100%); + border: 1px solid rgba(255, 143, 55, 0.4); + border-radius: 12px; + padding: 16px; + margin-bottom: 20px; + text-align: center; +} + +.yaknet-sso-login-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 12px; + background: linear-gradient(135deg, #ff8f37 0%, #ff5e3a 100%); + color: #fff !important; + font-size: 14px; + font-weight: 700; + padding: 12px 24px; + border-radius: 10px; + text-decoration: none; + width: 100%; + box-sizing: border-box; + box-shadow: 0 4px 18px rgba(255, 143, 55, 0.4); + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + cursor: pointer; + border: none; +} + +.yaknet-sso-login-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 24px rgba(255, 143, 55, 0.6); + color: #fff !important; +} + +.yaknet-header-widget { + display: inline-flex !important; + align-items: center !important; + position: relative !important; + z-index: 9999999 !important; + margin-left: 10px; +} + +yaknet-account { + display: inline-block !important; + position: relative !important; + z-index: 9999999 !important; +} + +my-header, +.header, +.header-right, +my-header .header-right, +my-header .user-menu, +my-header .action-buttons { + overflow: visible !important; +} + +.yaktube-admin-login-link { + margin-top: 15px; + text-align: center; + font-size: 12px; + color: #888; +} + +.yaktube-admin-login-link a { + color: #ff8f37; + text-decoration: none; +} + +.yaktube-admin-login-link a:hover { + text-decoration: underline; +}