feat(youtube): youtube çerez yönetimi ekle

YouTube cookies.txt dosyasını yönetmek için ayarlar arayüzü ve API uç noktaları eklendi.
- Kullanıcı arayüzünde çerez yükleme/kaydetme fonksiyonalitesi
- Cookies.txt dosyası için GET/POST API uç noktaları
- YouTube indirme işlemlerinde çerez desteği
- Çerez dosyası boyutu ve karakter validasyonu
- Ayarlar sayfasında sekme tabanlı arayüz
This commit is contained in:
2025-12-14 15:33:56 +03:00
parent de97b49dfa
commit 2f58ef5ef9
2 changed files with 376 additions and 8 deletions

View File

@@ -1,5 +1,79 @@
<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>
<section class="files">
@@ -8,7 +82,74 @@
<h2>Settings</h2>
</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>
<style>
@@ -31,10 +172,130 @@
gap: 8px;
}
.empty {
padding: 24px;
border: 1px dashed var(--border, #dcdcdc);
.tabs {
display: flex;
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;
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;
}
.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>