Files
dupe/client/src/routes/Settings.svelte
wisecolt 8bf08880fd feat(ph): pornhub için cookie yönetimi ekle
Pornhub video indirmelerinde yetkilendirme için cookie dosyası yönetimi
eklendi. Kullanıcılar arayüz üzerinden Netscape formatındaki cookies.txt
içeriğini kaydedebilir ve yönetebilir.

- Backend API /api/pornhub/cookies endpointleri eklendi
- Ayarlar sayfasına Pornhub sekmesi eklendi
- Cookie dosyası kontrolü ve yt-dlp entegrasyonu sağlandı
- Rabbit ikonu yenilendi ve boyutu artırıldı
- Docker portu 3005 olarak değiştirildi
2025-12-27 20:02:35 +03:00

543 lines
13 KiB
Svelte
Raw Blame History

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.
<script>
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: "pornhub", label: "Pornhub", icon: "fa-solid fa-lock" },
{ id: "advanced", label: "Advanced", icon: "fa-solid fa-gear" }
];
let activeTab = "youtube";
let youtubeCookies = "";
let cookiesUpdatedAt = null;
let loadingCookies = false;
let savingCookies = false;
let pornhubCookies = "";
let phCookiesUpdatedAt = null;
let loadingPhCookies = false;
let savingPhCookies = false;
let loadingYtSettings = false;
let savingYtSettings = false;
let selectedResolution = "1080p";
let onlyAudio = false;
const resolutionOptions = ["1080p", "720p", "480p", "360p", "240p", "144p"];
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;
}
}
async function loadPornhubCookies() {
loadingPhCookies = true;
error = null;
success = null;
try {
const resp = await apiFetch("/api/pornhub/cookies");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
pornhubCookies = data?.cookies || "";
phCookiesUpdatedAt = data?.updatedAt || null;
} catch (err) {
error = err?.message || "Cookies alınamadı.";
} finally {
loadingPhCookies = false;
}
}
async function savePornhubCookies() {
if (savingPhCookies) return;
error = null;
success = null;
savingPhCookies = true;
try {
const payload = {
cookies: pornhubCookies
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.join("\n")
};
const resp = await apiFetch("/api/pornhub/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}`);
}
phCookiesUpdatedAt = data.updatedAt || Date.now();
success = "Cookies kaydedildi.";
} catch (err) {
error = err?.message || "Cookies kaydedilemedi.";
} finally {
savingPhCookies = false;
}
}
async function loadYoutubeSettings() {
loadingYtSettings = true;
error = null;
try {
const resp = await apiFetch("/api/youtube/settings");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
if (data?.resolution) selectedResolution = data.resolution;
if (typeof data?.onlyAudio === "boolean") onlyAudio = data.onlyAudio;
} catch (err) {
error = err?.message || "YouTube ayarları alınamadı.";
} finally {
loadingYtSettings = false;
}
}
async function saveYoutubeSettings() {
if (savingYtSettings) return;
savingYtSettings = true;
error = null;
success = null;
try {
const resp = await apiFetch("/api/youtube/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
resolution: selectedResolution,
onlyAudio
})
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
throw new Error(data?.error || `HTTP ${resp.status}`);
}
success = "YouTube indirme ayarları kaydedildi.";
} catch (err) {
error = err?.message || "YouTube ayarları kaydedilemedi.";
} finally {
savingYtSettings = false;
}
}
onMount(() => {
loadCookies();
loadYoutubeSettings();
loadPornhubCookies();
});
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">
<div class="files-header">
<div class="header-title">
<h2>Settings</h2>
</div>
</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-solid fa-circle-down"></i>
<span>YouTube Download</span>
</div>
</div>
<div class="field inline compact left-align">
<div class="inline-field">
<label for="resolution">Çözünürlük</label>
<select
id="resolution"
bind:value={selectedResolution}
disabled={loadingYtSettings || savingYtSettings}
>
{#each resolutionOptions as res}
<option value={res}>{res}</option>
{/each}
</select>
</div>
<label class="checkbox-row">
<input
type="checkbox"
bind:checked={onlyAudio}
disabled={loadingYtSettings || savingYtSettings}
/>
<span>Only Audio (sadece ses indir)</span>
</label>
</div>
<div class="actions">
<button class="btn" on:click={loadYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
<i class="fa-solid fa-rotate"></i> Yenile
</button>
<button class="btn primary" on:click={saveYoutubeSettings} disabled={loadingYtSettings || savingYtSettings}>
<i class="fa-solid fa-floppy-disk"></i> Kaydet
</button>
</div>
</div>
<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 === "pornhub"}
<div class="card">
<div class="card-header">
<div class="title">
<i class="fa-solid fa-lock"></i>
<span>Pornhub Cookies</span>
</div>
{#if phCookiesUpdatedAt}
<div class="meta">Son güncelleme: {formatDate(phCookiesUpdatedAt)}</div>
{/if}
</div>
<div class="field">
<label for="ph-cookies">cookies.txt içeriği</label>
<textarea
id="ph-cookies"
spellcheck="false"
placeholder="Netscape HTTP Cookie formatında (cookies.txt) içerik girin"
bind:value={pornhubCookies}
></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={loadPornhubCookies} disabled={loadingPhCookies || savingPhCookies}>
<i class="fa-solid fa-rotate"></i> Yenile
</button>
<button class="btn primary" on:click={savePornhubCookies} disabled={loadingPhCookies || savingPhCookies}>
<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>
.files {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.files-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
}
.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.inline {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.field.inline.compact {
align-items: flex-end;
}
.field.inline.left-align {
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
}
.inline-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field select {
min-width: 180px;
max-width: 240px;
padding: 8px 10px;
border: 1px solid var(--border, #dcdcdc);
border-radius: 8px;
background: #fff;
}
.checkbox-row {
display: inline-flex;
align-items: center;
gap: 8px;
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;
}
.actions.left {
justify-content: flex-start;
}
.divider {
height: 1px;
background: #eee;
margin: 6px 0;
}
.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>