2026-08-29 12:42:36 +03:00
const { execFile } = require ( 'child_process' );
const path = require ( 'path' );
const http = require ( 'http' );
2026-09-19 05:14:11 +03:00
const fs = require ( 'fs' );
2026-08-29 12:42:36 +03:00
const searchCache = new Map ();
const CACHE_TTL = 30 * 60 * 1000 ; // 30 minutes
2026-09-19 05:14:11 +03:00
let cachedYtDlpPath = null ;
function resolveYtDlpPath ( logger ) {
if ( cachedYtDlpPath && ( cachedYtDlpPath === 'yt-dlp' || fs . existsSync ( cachedYtDlpPath ))) {
return cachedYtDlpPath ;
}
// 1. Environment variable override
if ( process . env . YTDLP_PATH && fs . existsSync ( process . env . YTDLP_PATH )) {
cachedYtDlpPath = process . env . YTDLP_PATH ;
return cachedYtDlpPath ;
}
// 2. Cross-platform candidates
const isWin = process . platform === 'win32' ;
const candidates = isWin
? [
'D:\\yaktube_storage\\bin\\yt-dlp.exe' ,
'C:\\laragon\\bin\\yt-dlp.exe' ,
path . join ( process . cwd (), 'bin' , 'yt-dlp.exe' ),
'yt-dlp.exe'
]
2026-09-19 05:20:33 +03:00
: [ '/usr/local/bin/yt-dlp' , '/usr/bin/yt-dlp' , '/bin/yt-dlp' , path . join ( process . cwd (), 'bin' , 'yt-dlp' ), 'yt-dlp' ];
2026-09-19 05:14:11 +03:00
for ( const candidate of candidates ) {
try {
if ( candidate . includes ( path . sep ) && fs . existsSync ( candidate )) {
cachedYtDlpPath = candidate ;
if ( logger ) logger . info ( `[YakTube OnDemand] Auto-detected yt-dlp binary at: ${ cachedYtDlpPath } ` );
return cachedYtDlpPath ;
}
} catch ( e ) {}
}
cachedYtDlpPath = isWin ? 'D:\\yaktube_storage\\bin\\yt-dlp.exe' : 'yt-dlp' ;
return cachedYtDlpPath ;
}
let cachedChannels = null ;
let channelsExpiresAt = 0 ;
async function getDefaultChannelId ( category ) {
// Category mapping defaults
const cat = String ( category || '' ). toLowerCase ();
if ( cat . includes ( 'tekno' ) || cat . includes ( 'tech' ) || cat . includes ( 'egitim' )) return '3299' ;
if ( cat . includes ( 'podcast' ) || cat . includes ( 'sohbet' )) return '3298' ;
if ( cat . includes ( 'genel' ) || cat . includes ( 'haber' )) return '1' ;
if ( cat . includes ( 'muzik' ) || cat . includes ( 'music' )) return '1146' ;
if ( cachedChannels && Date . now () < channelsExpiresAt ) {
return cachedChannels . defaultId || '1146' ;
}
2026-09-19 05:20:33 +03:00
return new Promise ( resolve => {
2026-09-19 05:14:11 +03:00
const req = http . request (
{
hostname : '127.0.0.1' ,
port : 9000 ,
path : '/api/v1/accounts/yaktube/video-channels' ,
method : 'GET' ,
headers : { Host : 'yaktube.yakhub.com.tr' }
},
res => {
let body = '' ;
res . on ( 'data' , c => ( body += c ));
res . on ( 'end' , () => {
try {
const data = JSON . parse ( body );
if ( data && Array . isArray ( data . data ) && data . data . length > 0 ) {
const musicCh = data . data . find ( c => c . name && c . name . includes ( 'muzik' ));
const defaultId = musicCh ? String ( musicCh . id ) : String ( data . data [ 0 ]. id );
cachedChannels = { defaultId , list : data . data };
channelsExpiresAt = Date . now () + 60 * 60 * 1000 ;
return resolve ( defaultId );
}
} catch ( e ) {}
resolve ( '1146' );
});
}
);
req . on ( 'error' , () => resolve ( '1146' ));
req . end ();
});
}
2026-08-29 12:42:36 +03:00
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 ();
});
}
2026-10-03 10:26:46 +03:00
async function register ({ getRouter , registerSetting , settingsManager , peertubeHelpers , logger }) {
2026-08-29 12:42:36 +03:00
const router = getRouter ();
2026-09-19 05:14:11 +03:00
const ytdlpPath = resolveYtDlpPath ( logger );
2026-08-29 12:42:36 +03:00
2026-10-03 10:26:46 +03:00
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 });
});
2026-08-29 12:42:36 +03:00
router . get ( '/search' , async ( req , res ) => {
const query = ( req . query . q || '' ). trim ();
if ( ! query || query . length < 2 ) {
2026-10-03 10:26:46 +03:00
return res . json ({ results : [], guestImportLimit });
2026-08-29 12:42:36 +03:00
}
const cached = getCached ( query );
if ( cached ) {
2026-10-03 10:26:46 +03:00
return res . json ({ results : cached , cached : true , guestImportLimit });
2026-08-29 12:42:36 +03:00
}
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 );
2026-10-03 10:26:46 +03:00
return res . json ({ results , cached : false , guestImportLimit });
2026-08-29 12:42:36 +03:00
} 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 ) => {
2026-10-03 10:26:46 +03:00
const { targetUrl , channelId , category , isAuthenticated , guestImportCount } = req . body ;
2026-08-29 12:42:36 +03:00
if ( ! targetUrl || ( ! targetUrl . includes ( 'youtube.com' ) && ! targetUrl . includes ( 'youtu.be' ))) {
return res . status ( 400 ). json ({ error : 'Invalid YouTube URL' });
}
2026-10-03 10:26:46 +03:00
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.'
});
}
}
2026-08-29 12:42:36 +03:00
try {
const token = await getAdminToken ();
2026-09-19 05:14:11 +03:00
const resolvedChannelId = channelId ? String ( channelId ) : await getDefaultChannelId ( category );
2026-08-29 12:42:36 +03:00
const postData = new URLSearchParams ({
targetUrl : targetUrl ,
2026-09-19 05:14:11 +03:00
channelId : resolvedChannelId ,
2026-08-29 12:42:36 +03:00
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
};