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
543 lines
13 KiB
Svelte
543 lines
13 KiB
Svelte
<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>
|