Merge pull request 'feat(youtube): youtube çerez yönetimi ekle' (#4) from develope into main
Reviewed-on: #4
This commit is contained in:
@@ -1,5 +1,79 @@
|
|||||||
<script>
|
<script>
|
||||||
// Tasarım diğer sayfalarla aynı iskelette; içerik placeholder.
|
import { onMount } from "svelte";
|
||||||
|
import { apiFetch } from "../utils/api.js";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "general", label: "General", icon: "fa-solid fa-sliders" },
|
||||||
|
{ id: "youtube", label: "YouTube", icon: "fa-brands fa-youtube" },
|
||||||
|
{ id: "advanced", label: "Advanced", icon: "fa-solid fa-gear" }
|
||||||
|
];
|
||||||
|
|
||||||
|
let activeTab = "youtube";
|
||||||
|
let youtubeCookies = "";
|
||||||
|
let cookiesUpdatedAt = null;
|
||||||
|
let loadingCookies = false;
|
||||||
|
let savingCookies = false;
|
||||||
|
let error = null;
|
||||||
|
let success = null;
|
||||||
|
|
||||||
|
async function loadCookies() {
|
||||||
|
loadingCookies = true;
|
||||||
|
error = null;
|
||||||
|
success = null;
|
||||||
|
try {
|
||||||
|
const resp = await apiFetch("/api/youtube/cookies");
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const data = await resp.json();
|
||||||
|
youtubeCookies = data?.cookies || "";
|
||||||
|
cookiesUpdatedAt = data?.updatedAt || null;
|
||||||
|
} catch (err) {
|
||||||
|
error = err?.message || "Cookies alınamadı.";
|
||||||
|
} finally {
|
||||||
|
loadingCookies = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCookies() {
|
||||||
|
if (savingCookies) return;
|
||||||
|
error = null;
|
||||||
|
success = null;
|
||||||
|
savingCookies = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
cookies: youtubeCookies
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n")
|
||||||
|
};
|
||||||
|
const resp = await apiFetch("/api/youtube/cookies", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
if (!resp.ok || !data?.ok) {
|
||||||
|
throw new Error(data?.error || `HTTP ${resp.status}`);
|
||||||
|
}
|
||||||
|
cookiesUpdatedAt = data.updatedAt || Date.now();
|
||||||
|
success = "Cookies kaydedildi.";
|
||||||
|
} catch (err) {
|
||||||
|
error = err?.message || "Cookies kaydedilemedi.";
|
||||||
|
} finally {
|
||||||
|
savingCookies = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
loadCookies();
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatDate(ts) {
|
||||||
|
if (!ts) return "—";
|
||||||
|
const d = new Date(Number(ts));
|
||||||
|
if (Number.isNaN(d.getTime())) return "—";
|
||||||
|
return d.toLocaleString();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="files">
|
<section class="files">
|
||||||
@@ -8,7 +82,74 @@
|
|||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty">Ayarlar içeriği yakında.</div>
|
|
||||||
|
<div class="tabs">
|
||||||
|
{#each tabs as tab}
|
||||||
|
<button
|
||||||
|
class:active={activeTab === tab.id}
|
||||||
|
class="tab"
|
||||||
|
type="button"
|
||||||
|
on:click={() => (activeTab = tab.id)}
|
||||||
|
>
|
||||||
|
<i class={tab.icon}></i>
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if activeTab === "youtube"}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="title">
|
||||||
|
<i class="fa-brands fa-youtube"></i>
|
||||||
|
<span>YouTube Cookies</span>
|
||||||
|
</div>
|
||||||
|
{#if cookiesUpdatedAt}
|
||||||
|
<div class="meta">Son güncelleme: {formatDate(cookiesUpdatedAt)}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="cookies">cookies.txt içeriği</label>
|
||||||
|
<textarea
|
||||||
|
id="cookies"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="Netscape HTTP Cookie formatında (cookies.txt) içerik girin"
|
||||||
|
bind:value={youtubeCookies}
|
||||||
|
></textarea>
|
||||||
|
<small>
|
||||||
|
Zararlı komut çalıştırılamaz; yalnızca düz metin cookie satırları yazılır.
|
||||||
|
Maksimum 20KB. Engellenen karakterler otomatik reddedilir.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn" on:click={loadCookies} disabled={loadingCookies || savingCookies}>
|
||||||
|
<i class="fa-solid fa-rotate"></i> Yenile
|
||||||
|
</button>
|
||||||
|
<button class="btn primary" on:click={saveCookies} disabled={loadingCookies || savingCookies}>
|
||||||
|
<i class="fa-solid fa-floppy-disk"></i> Kaydet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<div class="alert error">
|
||||||
|
<i class="fa-solid fa-circle-exclamation"></i>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if success}
|
||||||
|
<div class="alert success">
|
||||||
|
<i class="fa-solid fa-circle-check"></i>
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else if activeTab === "general"}
|
||||||
|
<div class="card muted">Genel ayarlar burada yer alacak.</div>
|
||||||
|
{:else if activeTab === "advanced"}
|
||||||
|
<div class="card muted">Gelişmiş ayarlar burada yer alacak.</div>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -31,10 +172,130 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.tabs {
|
||||||
padding: 24px;
|
display: flex;
|
||||||
border: 1px dashed var(--border, #dcdcdc);
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border, #dcdcdc);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border: 1px solid var(--border, #e0e0e0);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.muted {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .meta {
|
||||||
|
color: #666;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field textarea {
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border, #dcdcdc);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field small {
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border, #dcdcdc);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.primary {
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.error {
|
||||||
|
background: #ffe2e2;
|
||||||
|
color: #b30000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.success {
|
||||||
|
background: #e5ffe7;
|
||||||
|
color: #0f7a1f;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
113
server/server.js
113
server/server.js
@@ -72,6 +72,10 @@ const generatingThumbnails = new Set();
|
|||||||
const INFO_FILENAME = "info.json";
|
const INFO_FILENAME = "info.json";
|
||||||
const YT_ID_REGEX = /^[A-Za-z0-9_-]{11}$/;
|
const YT_ID_REGEX = /^[A-Za-z0-9_-]{11}$/;
|
||||||
const YT_DLP_BIN = process.env.YT_DLP_BIN || null;
|
const YT_DLP_BIN = process.env.YT_DLP_BIN || null;
|
||||||
|
const YT_COOKIES_PATH =
|
||||||
|
process.env.YT_DLP_COOKIES ||
|
||||||
|
process.env.YT_DLP_COOKIE_FILE ||
|
||||||
|
path.join(CACHE_DIR, "yt_cookies.txt");
|
||||||
let resolvedYtDlpBinary = null;
|
let resolvedYtDlpBinary = null;
|
||||||
const TMDB_API_KEY = process.env.TMDB_API_KEY;
|
const TMDB_API_KEY = process.env.TMDB_API_KEY;
|
||||||
const TMDB_BASE_URL = "https://api.themoviedb.org/3";
|
const TMDB_BASE_URL = "https://api.themoviedb.org/3";
|
||||||
@@ -734,10 +738,22 @@ function appendYoutubeLog(job, line) {
|
|||||||
|
|
||||||
function launchYoutubeJob(job) {
|
function launchYoutubeJob(job) {
|
||||||
const binary = getYtDlpBinary();
|
const binary = getYtDlpBinary();
|
||||||
const jsRuntime =
|
const jsRuntimeValue =
|
||||||
process.env.YT_DLP_JS_RUNTIME ||
|
process.env.YT_DLP_JS_RUNTIME ||
|
||||||
process.env.NODE_BIN ||
|
process.env.NODE_BIN ||
|
||||||
"/usr/bin/node";
|
"/usr/bin/node";
|
||||||
|
let jsRuntimeArg = jsRuntimeValue;
|
||||||
|
if (jsRuntimeValue && jsRuntimeValue.includes("=")) {
|
||||||
|
jsRuntimeArg = jsRuntimeValue;
|
||||||
|
} else if (jsRuntimeValue && jsRuntimeValue.includes(path.sep)) {
|
||||||
|
// Eğer yol verilmişse node=<path> formatına çevir
|
||||||
|
jsRuntimeArg = `node=${jsRuntimeValue}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieFile =
|
||||||
|
(YT_COOKIES_PATH && fs.existsSync(YT_COOKIES_PATH) && YT_COOKIES_PATH) ||
|
||||||
|
null;
|
||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
"-f",
|
"-f",
|
||||||
"bv+ba/b",
|
"bv+ba/b",
|
||||||
@@ -746,10 +762,13 @@ function launchYoutubeJob(job) {
|
|||||||
"jpg",
|
"jpg",
|
||||||
"--write-info-json",
|
"--write-info-json",
|
||||||
"--js-runtime",
|
"--js-runtime",
|
||||||
jsRuntime,
|
jsRuntimeArg,
|
||||||
|
...(cookieFile && fs.existsSync(cookieFile)
|
||||||
|
? ["--cookies", cookieFile]
|
||||||
|
: []),
|
||||||
job.url
|
job.url
|
||||||
];
|
];
|
||||||
job.debug = { binary, args, logs: [] };
|
job.debug = { binary, args, logs: [], jsRuntime: jsRuntimeArg, cookies: cookieFile };
|
||||||
const child = spawn(binary, args, {
|
const child = spawn(binary, args, {
|
||||||
cwd: job.savePath,
|
cwd: job.savePath,
|
||||||
env: process.env
|
env: process.env
|
||||||
@@ -5852,6 +5871,94 @@ app.post("/api/youtube/download", requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- 🎫 YouTube cookies yönetimi ---
|
||||||
|
app.get("/api/youtube/cookies", requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!YT_COOKIES_PATH || !fs.existsSync(YT_COOKIES_PATH)) {
|
||||||
|
return res.json({ hasCookies: false, cookies: null, updatedAt: null });
|
||||||
|
}
|
||||||
|
const stat = fs.statSync(YT_COOKIES_PATH);
|
||||||
|
const size = stat.size;
|
||||||
|
if (size > 20000) {
|
||||||
|
return res.json({ hasCookies: true, cookies: "", updatedAt: stat.mtimeMs });
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(YT_COOKIES_PATH, "utf-8");
|
||||||
|
res.json({ hasCookies: true, cookies: content, updatedAt: stat.mtimeMs });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("⚠️ YouTube cookies okunamadı:", err.message);
|
||||||
|
res.status(500).json({ error: "Cookies okunamadı" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/youtube/cookies", requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
let cookies = req.body?.cookies;
|
||||||
|
if (typeof cookies !== "string") {
|
||||||
|
return res.status(400).json({ error: "cookies alanı metin olmalı" });
|
||||||
|
}
|
||||||
|
cookies = cookies.replace(/\r\n/g, "\n");
|
||||||
|
if (cookies.length > 20000) {
|
||||||
|
return res.status(400).json({ error: "Cookie içeriği çok büyük (20KB sınırı)." });
|
||||||
|
}
|
||||||
|
if (/[^\x09\x0a\x0d\x20-\x7e]/.test(cookies)) {
|
||||||
|
return res.status(400).json({ error: "Cookie içeriğinde desteklenmeyen karakterler var." });
|
||||||
|
}
|
||||||
|
if (!YT_COOKIES_PATH) {
|
||||||
|
return res.status(500).json({ error: "Cookie yolu tanımlı değil." });
|
||||||
|
}
|
||||||
|
ensureDirForFile(YT_COOKIES_PATH);
|
||||||
|
fs.writeFileSync(YT_COOKIES_PATH, cookies, "utf-8");
|
||||||
|
res.json({ ok: true, updatedAt: Date.now() });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ YouTube cookies yazılamadı:", err.message);
|
||||||
|
res.status(500).json({ error: "Cookies kaydedilemedi" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- 🎫 YouTube cookies yönetimi ---
|
||||||
|
app.get("/api/youtube/cookies", requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!YT_COOKIES_PATH || !fs.existsSync(YT_COOKIES_PATH)) {
|
||||||
|
return res.json({ hasCookies: false, cookies: null, updatedAt: null });
|
||||||
|
}
|
||||||
|
const stat = fs.statSync(YT_COOKIES_PATH);
|
||||||
|
const size = stat.size;
|
||||||
|
if (size > 20000) {
|
||||||
|
return res.json({ hasCookies: true, cookies: "", updatedAt: stat.mtimeMs });
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(YT_COOKIES_PATH, "utf-8");
|
||||||
|
res.json({ hasCookies: true, cookies: content, updatedAt: stat.mtimeMs });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("⚠️ YouTube cookies okunamadı:", err.message);
|
||||||
|
res.status(500).json({ error: "Cookies okunamadı" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/youtube/cookies", requireAuth, (req, res) => {
|
||||||
|
try {
|
||||||
|
let cookies = req.body?.cookies;
|
||||||
|
if (typeof cookies !== "string") {
|
||||||
|
return res.status(400).json({ error: "cookies alanı metin olmalı" });
|
||||||
|
}
|
||||||
|
cookies = cookies.replace(/\r\n/g, "\n");
|
||||||
|
if (cookies.length > 20000) {
|
||||||
|
return res.status(400).json({ error: "Cookie içeriği çok büyük (20KB sınırı)." });
|
||||||
|
}
|
||||||
|
if (/[^\x09\x0a\x0d\x20-\x7e]/.test(cookies)) {
|
||||||
|
return res.status(400).json({ error: "Cookie içeriğinde desteklenmeyen karakterler var." });
|
||||||
|
}
|
||||||
|
if (!YT_COOKIES_PATH) {
|
||||||
|
return res.status(500).json({ error: "Cookie yolu tanımlı değil." });
|
||||||
|
}
|
||||||
|
ensureDirForFile(YT_COOKIES_PATH);
|
||||||
|
fs.writeFileSync(YT_COOKIES_PATH, cookies, "utf-8");
|
||||||
|
res.json({ ok: true, updatedAt: Date.now() });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ YouTube cookies yazılamadı:", err.message);
|
||||||
|
res.status(500).json({ error: "Cookies kaydedilemedi" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- 📺 TV dizileri listesi ---
|
// --- 📺 TV dizileri listesi ---
|
||||||
app.get("/api/tvshows", requireAuth, (req, res) => {
|
app.get("/api/tvshows", requireAuth, (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user