Files
peertube-plugin-yaktube-ond…/main.js
T

404 lines
13 KiB
JavaScript

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const { execFile } = require('child_process');
const path = require('path');
const http = require('http');
const fs = require('fs');
const searchCache = new Map();
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
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'
]
: ['/usr/local/bin/yt-dlp', '/usr/bin/yt-dlp', '/bin/yt-dlp', path.join(process.cwd(), 'bin', 'yt-dlp'), 'yt-dlp'];
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';
}
return new Promise(resolve => {
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();
});
}
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, registerSetting, settingsManager, peertubeHelpers, logger }) {
const router = getRouter();
const ytdlpPath = resolveYtDlpPath(logger);
let guestImportLimit = 5;
if (typeof registerSetting === 'function') {
registerSetting({
name: 'guest-import-limit',
label: 'Misafir Video İndirme Sınırı (Kota)',
type: 'input',
description:
'Giriş yapmamış (misafir) kullanıcıların YouTube üzerinden sunucuya aktarabileceği maksimum video sayısı (Örn: 5). Sınır istemiyorsanız (sınırsız) 0 yazın.',
private: false,
default: '5'
});
}
async function loadPluginSettings() {
if (!settingsManager || typeof settingsManager.getSetting !== 'function') return;
try {
const val = await settingsManager.getSetting('guest-import-limit');
if (val !== undefined && val !== null && String(val).trim() !== '') {
const parsed = parseInt(String(val).trim(), 10);
guestImportLimit = isNaN(parsed) || parsed < 0 ? 5 : parsed;
} else {
guestImportLimit = 5;
}
} catch (e) {}
}
await loadPluginSettings();
if (settingsManager && typeof settingsManager.onSettingsChange === 'function') {
settingsManager.onSettingsChange(loadPluginSettings);
}
router.get('/quota-config', (_req, res) => {
return res.json({ ok: true, guestImportLimit });
});
router.get('/search', async (req, res) => {
const query = (req.query.q || '').trim();
if (!query || query.length < 2) {
return res.json({ results: [], guestImportLimit });
}
const cached = getCached(query);
if (cached) {
return res.json({ results: cached, cached: true, guestImportLimit });
}
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, guestImportLimit });
} catch (parseErr) {
if (logger) logger.error('JSON parse error in search: ' + parseErr.message);
return res.status(500).json({ error: 'Failed to parse search results' });
}
});
});
router.post('/import', async (req, res) => {
const { targetUrl, channelId, category, isAuthenticated, guestImportCount } = req.body;
if (!targetUrl || (!targetUrl.includes('youtube.com') && !targetUrl.includes('youtu.be'))) {
return res.status(400).json({ error: 'Invalid YouTube URL' });
}
const authHeader = req.headers && req.headers.authorization;
const isLoggedUser = Boolean(isAuthenticated || (authHeader && authHeader.startsWith('Bearer ')));
if (!isLoggedUser && guestImportLimit > 0) {
const clientCount = parseInt(String(guestImportCount || '0'), 10) || 0;
if (clientCount >= guestImportLimit) {
return res.status(429).json({
ok: false,
quotaExceeded: true,
limit: guestImportLimit,
error: 'Kota Doldu: Lütfen devam etmek için giriş yapın.'
});
}
}
try {
const token = await getAdminToken();
const resolvedChannelId = channelId ? String(channelId) : await getDefaultChannelId(category);
const postData = new URLSearchParams({
targetUrl: targetUrl,
channelId: resolvedChannelId,
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
};