Compare commits

...

2 Commits

Author SHA1 Message Date
b63bd41973 feat(rabbit): video oynatıcıya hata ayıklama logları ekle 2025-12-20 00:25:48 +03:00
a9459cc4fe feat(rabbit): PH video indirme ve yönetim özelliği ekle
PH video indirme, yönetim ve oynatma özelliği eklendi.
Yeni Rabbit sayfası ile indirilen videolar listelenebilir ve oynatılabilir.
Kenar menüye Rabbit sekmesi eklendi, dinamik olarak göster/gizle.
Transferler sayfasına PH URL desteği eklendi.
WebSocket üzerinden Rabbit sayısı güncellemeleri sağlandı.
Dosya görünümü Rabbit içeriklerini filtreleyecek şekilde güncellendi.
Arka planda Rabbit metadata yönetimi ve dosya sistemi entegrasyonu.
2025-12-15 23:44:37 +03:00
9 changed files with 751 additions and 54 deletions

11
client/public/rabbit.svg Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="366" height="416">
<path d="M0 0 C120.78 0 241.56 0 366 0 C366 137.28 366 274.56 366 416 C245.22 416 124.44 416 0 416 C0 278.72 0 141.44 0 0 Z " fill="#FEFEFC" transform="translate(0,0)"/>
<path d="M0 0 C2.74947486 14.33464078 3.23773222 28.75501149 3.25 43.3125 C3.25067474 44.04545288 3.25134949 44.77840576 3.25204468 45.53356934 C3.20308951 79.39224522 -2.59823624 114.18793279 -19.00585938 144.1796875 C-19.96808738 145.94156667 -20.89870425 147.72059617 -21.828125 149.5 C-27.92109878 160.76432128 -35.60944494 172.20205278 -45 181 C-43.52122253 184.43757695 -41.55955643 186.60482138 -38.9375 189.25 C-29.91171159 199.13469408 -24.0754566 211.02084088 -21 224 C-20.8256543 224.72703125 -20.65130859 225.4540625 -20.47167969 226.203125 C-19.89055452 229.64896864 -19.81916545 233.0354898 -19.796875 236.51953125 C-19.78995132 237.24698044 -19.78302765 237.97442963 -19.77589417 238.72392273 C-19.75856834 241.08683636 -19.75229548 243.44952565 -19.75 245.8125 C-19.74866058 246.61768066 -19.74732117 247.42286133 -19.74594116 248.25244141 C-19.76199848 292.78768997 -19.76199848 292.78768997 -29 307 C-46.26921209 322.21408383 -75.50567493 329.70004054 -97 337 C-97.33 330.4 -97.66 323.8 -98 317 C-100.27777344 318.10859375 -102.55554688 319.2171875 -104.90234375 320.359375 C-107.13629863 321.44534962 -109.37050545 322.53080427 -111.60473633 323.61621094 C-113.14406236 324.36436689 -114.68313054 325.11305362 -116.22192383 325.86230469 C-139.09875 337 -139.09875 337 -142 337 C-146.72704527 323.3464362 -146.34449006 314.99509829 -143 301 C-144.59714844 300.56300781 -144.59714844 300.56300781 -146.2265625 300.1171875 C-166.7670329 294.37433198 -190.07284079 285.51045475 -201.71484375 266.3125 C-202.13894531 265.549375 -202.56304687 264.78625 -203 264 C-203.41378906 263.29875 -203.82757813 262.5975 -204.25390625 261.875 C-208.78374965 252.78374636 -208.46638456 240.95913725 -206.08203125 231.26953125 C-204.896602 228.05851417 -203.5416312 225.05448528 -202 222 C-201.58878906 221.09378906 -201.17757812 220.18757812 -200.75390625 219.25390625 C-189.64286629 196.53726966 -167.04389016 182.0073024 -143.94921875 173.60546875 C-133.01269725 169.88718511 -121.45099615 167.47249472 -110 166 C-110.86238281 165.59910156 -111.72476563 165.19820312 -112.61328125 164.78515625 C-115.23146791 163.56736958 -117.84658516 162.34318099 -120.4609375 161.1171875 C-123.51944607 159.68472146 -126.58788205 158.27688679 -129.6640625 156.8828125 C-156.20484063 144.74863826 -180.28842831 127.45399532 -198 104 C-198.721875 103.10539062 -199.44375 102.21078125 -200.1875 101.2890625 C-220.93319885 74.99677055 -229.6899346 43.95848735 -231 11 C-225.29450616 12.78189597 -219.89176814 14.79159228 -214.5625 17.5 C-213.55050537 18.01167725 -213.55050537 18.01167725 -212.51806641 18.53369141 C-194.95624066 27.59020223 -178.44076714 38.49826342 -164 52 C-162.6927439 53.15181239 -161.38044984 54.29793933 -160.0625 55.4375 C-145.24829106 68.39243575 -145.24829106 68.39243575 -139.7265625 74.9609375 C-137.79505877 77.24203467 -135.77584517 79.42735171 -133.75 81.625 C-125.49369482 90.80021942 -118.15394156 100.71508957 -110.8125 110.625 C-110.25473877 111.37579834 -109.69697754 112.12659668 -109.12231445 112.90014648 C-105.33665103 118.04667294 -101.75827911 123.30845127 -98.2331543 128.63623047 C-95.72361911 132.4265052 -93.17543913 136.190297 -90.625 139.953125 C-90.16538376 140.6314035 -89.70576752 141.30968201 -89.23222351 142.0085144 C-87.82184073 144.08925283 -86.41093346 146.16963496 -85 148.25 C-84.02469772 149.68877371 -83.04943706 151.12757562 -82.07421875 152.56640625 C-79.71662388 156.04459517 -77.35847159 159.52240551 -75 163 C-74.98018066 162.28682617 -74.96036133 161.57365234 -74.93994141 160.83886719 C-73.22308882 101.74499076 -58.91767279 52.8185497 -15.08740234 10.91748047 C-3.20244461 0 -3.20244461 0 0 0 Z " fill="#010101" transform="translate(290,21)"/>
<path d="M0 0 C4.20053076 0.6083594 7.3244102 2.06494699 11.04296875 4.08203125 C12.19990234 4.68337891 13.35683594 5.28472656 14.54882812 5.90429688 C16.37110891 6.85331462 18.1849871 7.8111157 19.98461914 8.80249023 C26.99529492 12.8657268 26.99529492 12.8657268 34.7956543 13.64086914 C37.37807396 12.63197573 39.62844123 11.43160082 42 10 C43.45255463 9.28895815 44.90946344 8.58672508 46.37109375 7.89453125 C47.72870645 7.20472422 49.08421473 6.51075852 50.4375 5.8125 C57.64446367 2.11851211 57.64446367 2.11851211 61 1 C63.33770954 17.40935917 64.15277604 33.42732512 64 50 C58.27309115 46.88804819 53.06589827 43.28768225 47.8125 39.4375 C47.01908203 38.86064453 46.22566406 38.28378906 45.40820312 37.68945312 C39.30799808 33.24874971 39.30799808 33.24874971 36.90844727 31.31079102 C35.02626375 29.74841864 35.02626375 29.74841864 32.3828125 30.03125 C27.64994736 31.38662016 23.32329256 33.40859025 18.875 35.5 C17.10296287 36.32592784 15.32957253 37.14896006 13.5546875 37.96875 C12.77593262 38.33419922 11.99717773 38.69964844 11.19482422 39.07617188 C9.13794156 39.94193872 7.17684468 40.5265843 5 41 C3.03724434 34.04501798 2.1688905 27.11086776 1.375 19.9375 C1.2409375 18.79216797 1.106875 17.64683594 0.96875 16.46679688 C0.34291769 10.94606186 -0.17742277 5.55655857 0 0 Z " fill="#030303" transform="translate(118,353)"/>
<path d="M0 0 C1.82793089 1.82793089 1.34619549 4.35280095 1.5234375 6.796875 C0.70079937 10.25932206 -1.34236395 11.70065694 -4 14 C-6.46047723 15.0596372 -8.68212063 15.87885131 -11.23046875 16.61328125 C-11.93066132 16.82898773 -12.63085388 17.04469421 -13.3522644 17.26693726 C-15.62642942 17.96376334 -17.90654436 18.63827139 -20.1875 19.3125 C-21.75947538 19.78931612 -23.33100633 20.26759982 -24.90209961 20.74731445 C-28.07337655 21.71341459 -31.24699716 22.6711293 -34.42285156 23.62207031 C-38.63175994 24.88472074 -42.82949758 26.18078393 -47.0234375 27.4921875 C-49.369782 28.22398932 -51.71613556 28.95576212 -54.0625 29.6875 C-55.14877686 30.02628174 -56.23505371 30.36506348 -57.35424805 30.71411133 C-58.34207275 31.01857178 -59.32989746 31.32303223 -60.34765625 31.63671875 C-61.63168335 32.03443481 -61.63168335 32.03443481 -62.94165039 32.44018555 C-65 33 -65 33 -67 33 C-67 28.71 -67 24.42 -67 20 C-59.96119668 17.09096984 -52.905837 14.60264754 -45.62890625 12.36328125 C-44.58476059 12.03837204 -43.54061493 11.71346283 -42.46482849 11.37870789 C-39.16521698 10.35284134 -35.86391843 9.33253477 -32.5625 8.3125 C-30.32084609 7.61691487 -28.07930849 6.92095475 -25.83789062 6.22460938 C-22.69292243 5.24762427 -19.54779199 4.27120146 -16.40174866 3.29768372 C-15.47325607 3.01035873 -14.54476349 2.72303375 -13.58813477 2.42700195 C-12.7722438 2.174814 -11.95635284 1.92262604 -11.11573792 1.66279602 C-7.16567854 0.42536315 -4.17162396 -0.18137495 0 0 Z " fill="#040504" transform="translate(260,350)"/>
<path d="M0 0 C4.30278235 1.72111294 7.98533336 4.76424595 10 9 C10.89812481 14.64424988 10.77100145 19.37233026 8.3125 24.5625 C5.22905688 27.81261572 2.59681441 29.71936095 -1.8671875 30.50390625 C-10.99170481 30.68165659 -10.99170481 30.68165659 -15.125 27.3125 C-18.91308392 23.34929785 -20.07550405 19.98972959 -20.375 14.5625 C-20.253882 10.00543517 -19.06661546 7.47011749 -16 4 C-11.02351528 -0.50253379 -6.42427681 -0.87309802 0 0 Z " fill="#FAFBF9" transform="translate(148,220)"/>
<path d="M0 0 C0.66 0 1.32 0 2 0 C2 8.25 2 16.5 2 25 C0.35 23.35 -1.3 21.7 -3 20 C-2.505 18.515 -2.505 18.515 -2 17 C-1.741247 13.54277237 -1.55711958 10.09143556 -1.39453125 6.62890625 C-1.10148709 1.10148709 -1.10148709 1.10148709 0 0 Z " fill="#E1E1DE" transform="translate(214,160)"/>
<path d="M0 0 C0.66 0 1.32 0 2 0 C1.34 4.29 0.68 8.58 0 13 C-0.33 13 -0.66 13 -1 13 C-1.02686553 11.02090602 -1.04633375 9.04171029 -1.0625 7.0625 C-1.07410156 5.96035156 -1.08570313 4.85820312 -1.09765625 3.72265625 C-1 1 -1 1 0 0 Z " fill="#F2F2EE" transform="translate(214,160)"/>
<path d="M0 0 C0.66 0.33 1.32 0.66 2 1 C-2.30769231 4.93846154 -2.30769231 4.93846154 -5.8125 5.25 C-6.534375 5.1675 -7.25625 5.085 -8 5 C-5.45378681 2.99940392 -3.01962385 1.20784954 0 0 Z " fill="#E3E4DF" transform="translate(183,341)"/>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -6,6 +6,7 @@
import Files from "./routes/Files.svelte";
import Transfers from "./routes/Transfers.svelte";
import Trash from "./routes/Trash.svelte";
import Rabbit from "./routes/Rabbit.svelte";
import Movies from "./routes/Movies.svelte";
import TvShows from "./routes/TvShows.svelte";
import Music from "./routes/Music.svelte";
@@ -150,6 +151,7 @@
<Route path="/movies" component={Movies} />
<Route path="/tv" component={TvShows} />
<Route path="/music" component={Music} />
<Route path="/rabbit" component={Rabbit} />
<Route path="/profile" component={Profile} />
<Route path="/settings" component={Settings} />
<Route path="/transfers" component={Transfers} />

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor">
<path d="M342.5 46.2c-9.9 2.3-18.8 7-28.9 14.4-8.5 6.3-24.5 22.1-30.7 30-4.7 6-5.9 9-5.3 12.3 1.1 6.2 7 12 12.5 12 6.6 0 17.1-7.3 28.3-20 12-13.8 16-22.5 16-35.3 0-8-.2-8.4-2-12.2-3.5-7.3-12-11.4-19.9-9.2zm-97 50.6c-6.4 4.2-9.2 11.8-7.2 19 2 7.3 6.8 10.4 18.6 12.2 11.6 1.8 17 4.4 17 8.4 0 2.2-5.4 10-18.1 26-9.9 12.5-20.9 28.4-24.4 35.4-2.3 4.7-2.7 6.7-2.3 11.5 1 11.2 7.7 20.4 17.9 24.6 3.7 1.5 6.6 1.8 14.3 1.6 10.3-.2 19.1-2 29.6-6 14.7-5.7 24.6-12.8 38.3-27.1 18.1-19 25.7-33.8 25.6-51.4-.1-12.6-2.6-20.4-9-28-9-10.7-23.5-17-45.3-19.5-7.4-.9-14.2-2.3-18.2-3.8-7.3-2.7-10.5-5.7-10.5-9.7 0-2.7 1.5-5.3 9-15.8 5-7 10.2-15.2 11.6-18.1 1.5-3 2.7-8.5 3-13.9.6-9.4-.4-12.9-5.4-19-7.4-8.9-21.7-11.4-32.3-5.6zM230 250.6c-1.3.7-5 4.2-8.2 7.7-10.2 11-14.5 20.1-14.5 31.4.1 9.4 2.5 15.6 11.2 27.9 6.4 9.1 7 10.2 7 14.9 0 4.4-.8 6.6-5.3 14-15.5 25.8-20.7 43.7-15.4 54.9 5.7 11.8 21.6 17.7 33.6 12.6 6.3-2.7 9.5-6.1 16.9-18.5 4.1-7 8.5-13.7 9.7-14.9 1.8-1.8 2.7-2 5.1-1.3 9.5 3 13.4 15.4 8.6 26.9-2.9 7.2-7.5 11.9-16 16.3-7.6 4-7.9 4.2-7.9 7.8 0 3.4.2 3.7 3.8 5.2 6.3 2.7 18.3 4.6 28.2 4.6 23.1 0 38.4-9.2 52-32.4 10.1-17.2 16.8-35.2 20.1-54.7 1.8-10.9 1.5-35.8-.5-47.3-2.5-14.3-6.2-26-12.6-38.6-10-19.8-22.8-32.9-42.5-43.2-13.3-6.9-17.6-7.8-33.5-7.8-13.7 0-15.3.2-16.9 1.8-.9 1-6.9 10.8-13.4 21.8-12.1 20.6-14.2 23.7-18.5 27.8-6.7 6.4-14.9 10.3-24.4 11.8-5.2.9-7.3.5-10.1-1-2-.9-3.6-1.6-3.7-1.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,11 +1,12 @@
<script>
import { Link } from "svelte-routing";
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
import { Link } from "svelte-routing";
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
import { movieCount } from "../stores/movieStore.js";
import { tvShowCount } from "../stores/tvStore.js";
import { musicCount } from "../stores/musicStore.js";
import { trashCount } from "../stores/trashStore.js";
import { apiFetch } from "../utils/api.js";
import { rabbitCount } from "../stores/rabbitStore.js";
import { trashCount } from "../stores/trashStore.js";
import { apiFetch, getAccessToken } from "../utils/api.js";
export let menuOpen = false;
const dispatch = createEventDispatcher();
@@ -13,12 +14,13 @@ import { musicCount } from "../stores/musicStore.js";
let hasShows = false;
let hasTrash = false;
let hasMusic = false;
let hasRabbit = false;
// Svelte store kullanarak reaktivite sağla
import { writable } from 'svelte/store';
const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 });
let diskSpace;
let hasMedia = false;
$: hasMedia = hasMovies || hasShows || hasMusic;
let hasMedia = false;
$: hasMedia = hasMovies || hasShows || hasMusic || hasRabbit;
// Store subscription'ı temizlemek için
let unsubscribeDiskSpace;
@@ -50,12 +52,16 @@ const unsubscribeTrash = trashCount.subscribe((count) => {
const unsubscribeMusic = musicCount.subscribe((count) => {
hasMusic = (count ?? 0) > 0;
});
const unsubscribeRabbit = rabbitCount.subscribe((count) => {
hasRabbit = (count ?? 0) > 0;
});
onDestroy(() => {
unsubscribeMovie();
unsubscribeTv();
unsubscribeTrash();
unsubscribeMusic();
unsubscribeRabbit();
if (unsubscribeDiskSpace) {
unsubscribeDiskSpace();
}
@@ -84,52 +90,64 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
// Component yüklendiğinde disk space bilgilerini al
onMount(() => {
console.log('🔌 Sidebar component mounted');
fetchDiskSpace();
// WebSocket bağlantısı kur
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Server port'unu doğru almak için
fetchRabbitCount();
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const currentHost = window.location.host;
// Eğer client farklı portta çalışıyorsa, server port'unu manuel belirt
const wsHost = currentHost.includes(':3000') ? currentHost.replace(':3000', ':3001') : currentHost;
const wsUrl = `${wsProtocol}//${wsHost}`;
console.log('🔌 Connecting to WebSocket at:', wsUrl);
// WebSocket bağlantısını global olarak saklayalım
const wsHost = currentHost.includes(":3000")
? currentHost.replace(":3000", ":3001")
: currentHost;
const token = getAccessToken() || "";
const wsUrl = `${wsProtocol}//${wsHost}?token=${token}`;
window.diskSpaceWs = new WebSocket(wsUrl);
window.diskSpaceWs.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('WebSocket message received:', data);
if (data.type === 'diskSpace') {
console.log('Disk space update received:', data.data);
if (data.type === "diskSpace") {
updateDiskSpace(data.data);
} else if (data.type === "rabbitCount") {
rabbitCount.set(data.count || 0);
hasRabbit = (data.count || 0) > 0;
}
} catch (err) {
console.error('WebSocket message parse error:', err);
console.error("WebSocket message parse error:", err);
}
};
window.diskSpaceWs.onopen = () => {
console.log('WebSocket connected for disk space updates');
};
window.diskSpaceWs.onopen = () => {};
window.diskSpaceWs.onerror = (error) => {
console.error('WebSocket error:', error);
console.error("WebSocket error:", error);
};
window.diskSpaceWs.onclose = () => {
console.log('WebSocket disconnected');
};
window.diskSpaceWs.onclose = () => {};
onDestroy(() => {
if (window.diskSpaceWs && (window.diskSpaceWs.readyState === WebSocket.OPEN || window.diskSpaceWs.readyState === WebSocket.CONNECTING)) {
if (
window.diskSpaceWs &&
(window.diskSpaceWs.readyState === WebSocket.OPEN ||
window.diskSpaceWs.readyState === WebSocket.CONNECTING)
) {
window.diskSpaceWs.close();
}
});
});
async function fetchRabbitCount() {
try {
const resp = await apiFetch("/api/rabbit");
if (!resp.ok) return;
const data = await resp.json().catch(() => null);
const count = data?.count ?? data?.items?.length ?? 0;
rabbitCount.set(count);
hasRabbit = count > 0;
} catch (err) {
// ignore
}
}
</script>
<div class="sidebar" class:open={menuOpen}>
@@ -224,6 +242,20 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
Music
</Link>
{/if}
{#if hasRabbit}
<Link
to="/rabbit"
class="item"
getProps={({ isCurrent }) => ({
class: isCurrent ? "item active" : "item",
})}
on:click={handleLinkClick}
>
<img src="/rabbit.svg" alt="Rabbit" class="icon rabbit-icon" />
Rabbit
</Link>
{/if}
</div>
{/if}
@@ -304,4 +336,10 @@ const unsubscribeMusic = musicCount.subscribe((count) => {
border-radius: 9px;
line-height: 1;
}
.rabbit-icon {
width: 18px;
height: 18px;
object-fit: contain;
}
</style>

View File

@@ -303,6 +303,20 @@
}
function updateVisibleState(fileList, path = currentPath) {
const rabbitCompletedRoots = new Set(
fileList
.filter(
(f) =>
!f.isDirectory &&
(f.displayName === ".ph_complete" || f.name?.endsWith("/.ph_complete"))
)
.map((f) => {
const segs = f.displaySegments || f.name?.split("/") || [];
return segs.length ? segs[0] : null;
})
.filter(Boolean)
);
const dirs = buildDirectoryEntries(fileList);
const directoryMap = new Map();
dirs.forEach((dir) => {
@@ -334,7 +348,16 @@
(file) =>
!file.isDirectory &&
normalizePath(file.displayParentPath) === normalizePath(path) &&
file.displayName.toLowerCase() !== "info.js",
file.displayName.toLowerCase() !== "info.js" &&
!file.displayName.toLowerCase().includes(".part") &&
file.displayName !== ".ph_complete" &&
!(() => {
const segs = file.displaySegments || [];
const root = segs.length ? segs[0] : "";
const isRabbit = root.startsWith("ph_");
const isMp4 = file.displayName.toLowerCase().endsWith(".mp4");
return isRabbit && isMp4 && !rabbitCompletedRoots.has(root);
})()
);
applyOrdering(path);
breadcrumbs = computeBreadcrumbs(path);

View File

@@ -0,0 +1,244 @@
<script>
import { onMount } from "svelte";
import { apiFetch, withToken, API } from "../utils/api.js";
import { setRabbitCount } from "../stores/rabbitStore.js";
let items = [];
let loading = true;
let error = null;
let selected = null;
let showPlayer = false;
async function load() {
loading = true;
error = null;
try {
const resp = await apiFetch("/api/rabbit");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
items = data?.items || [];
setRabbitCount(items.length);
} catch (err) {
error = err?.message || "Rabbit listesi alınamadı";
} finally {
loading = false;
}
}
onMount(load);
function thumbUrl(item) {
if (!item.thumbnail) return null;
const url = `${API}${item.thumbnail}`;
return withToken(url);
}
function videoUrl(item) {
if (!item?.file || !item?.folderId) return null;
const safeFolder = encodeURIComponent(item.folderId.trim());
const segments = String(item.file)
.split("/")
.filter(Boolean)
.map((seg) => encodeURIComponent(seg.trim()));
const relPath = segments.join("/");
const url = `${API}/downloads/${safeFolder}/${relPath}`;
// Debug: console'a URL yazdır
console.log("🎥 Video URL Debug:", {
folderId: item.folderId,
file: item.file,
safeFolder,
relPath,
finalUrl: withToken(url)
});
return withToken(url);
}
function playItem(item) {
selected = item;
showPlayer = true;
}
</script>
<section class="rabbit-page">
<div class="header">
<h2>Rabbit</h2>
<button class="btn" on:click={load} disabled={loading}>
<i class="fa-solid fa-rotate"></i> Yenile
</button>
</div>
{#if loading}
<div class="state">Yükleniyor...</div>
{:else if error}
<div class="state error">{error}</div>
{:else if items.length === 0}
<div class="state">Henüz içerik yok.</div>
{:else}
<div class="grid">
{#each items as item (item.id)}
<div class="card" on:click={() => playItem(item)}>
{#if thumbUrl(item)}
<img class="thumb" src={thumbUrl(item)} alt={item.title} />
{:else}
<div class="thumb placeholder"><i class="fa-regular fa-image"></i></div>
{/if}
<div class="info">
<div class="title" title={item.title}>{item.title}</div>
{#if item.added}
<div class="meta">{new Date(item.added).toLocaleString()}</div>
{/if}
</div>
</div>
{/each}
</div>
{/if}
{#if showPlayer && selected}
<div class="player" on:click={() => (showPlayer = false)}>
<div class="player-content" on:click|stopPropagation>
<button class="player-close" on:click={() => (showPlayer = false)}>Kapat</button>
<div class="player-title">{selected.title}</div>
{#if videoUrl(selected)}
<video
controls
autoplay
src={videoUrl(selected)}
on:error={(e) => {
console.error("🎥 Video Error:", e.target.error);
console.log("🎥 Failed src:", videoUrl(selected));
}}
on:loadeddata={() => {
console.log("🎥 Video loaded successfully");
}}
></video>
{:else}
<div class="state error">Video yolu bulunamadı</div>
{/if}
</div>
</div>
{/if}
</section>
<style>
.rabbit-page {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 1px solid #dcdcdc;
border-radius: 8px;
background: #f7f7f7;
cursor: pointer;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.card {
position: relative;
background: #f5f5f5;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
isolation: isolate;
transition: box-shadow 0.18s ease;
cursor: pointer;
}
.thumb {
width: 100%;
height: 180px;
object-fit: cover;
background: #f1f1f1;
}
.thumb.placeholder {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 28px;
}
.info {
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.title {
font-weight: 700;
font-size: 13px;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.meta {
font-size: 12px;
color: #666;
}
.player {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.player-content {
background: #111;
padding: 12px;
border-radius: 10px;
max-width: 900px;
width: 90%;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
display: flex;
flex-direction: column;
gap: 10px;
}
.player video {
width: 100%;
max-height: 520px;
border-radius: 8px;
background: #000;
}
.player-title {
font-weight: 700;
font-size: 15px;
color: #fff;
}
.player-close {
align-self: flex-end;
background: #fff;
border: none;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
}
.state {
padding: 20px;
text-align: center;
color: #666;
}
.state.error {
color: #b00020;
}
</style>

View File

@@ -75,6 +75,23 @@
}
const YT_VIDEO_ID_RE = /^[A-Za-z0-9_-]{11}$/;
function normalizePornhubUrl(value) {
if (!value || typeof value !== "string") return null;
try {
const url = new URL(value.trim());
if (url.protocol !== "https:") return null;
const host = url.hostname.toLowerCase();
if (host !== "pornhub.com" && host !== "www.pornhub.com") return null;
if (url.pathname !== "/view_video.php") return null;
const viewkey = url.searchParams.get("viewkey");
if (!viewkey) return null;
return `https://www.pornhub.com/view_video.php?viewkey=${encodeURIComponent(
viewkey
)}`;
} catch {
return null;
}
}
function isMagnetLink(value) {
if (!value || typeof value !== "string") return false;
@@ -99,7 +116,7 @@
}
async function handleUrlInput() {
const input = prompt("Magnet veya YouTube URL girin:");
const input = prompt("Magnet, YouTube veya Pornhub URL girin:");
if (!input) return;
if (isMagnetLink(input)) {
await apiFetch("/api/transfer", {
@@ -125,8 +142,23 @@
await list();
return;
}
const normalizedPh = normalizePornhubUrl(input);
if (normalizedPh) {
const resp = await apiFetch("/api/pornhub/download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: normalizedPh })
});
if (!resp.ok) {
const data = await resp.json().catch(() => null);
alert(data?.error || "Pornhub indirmesi başlatılamadı");
return;
}
await list();
return;
}
alert(
"Yalnızca magnet linkleri veya https://www.youtube.com/watch?v=... formatındaki YouTube URL'leri destekleniyor."
"Yalnızca magnet linkleri, YouTube (https://www.youtube.com/watch?v=...) veya Pornhub (https://www.pornhub.com/view_video.php?viewkey=...) URL'leri destekleniyor."
);
}
@@ -556,7 +588,11 @@
class="thumb"
on:load={(e) => e.target.classList.add("loaded")}
/>
{:else if t.type === "youtube" && (!t.progress || t.progress <= 0)}
{:else if (t.type === "pornhub" && (!t.progress || t.progress <= 0))}
<div class="thumb placeholder loading">
<div class="spinner"></div>
</div>
{:else if (t.type === "youtube" && (!t.progress || t.progress <= 0))}
<div class="thumb placeholder loading">
<div class="spinner"></div>
</div>
@@ -568,8 +604,8 @@
<div class="torrent-info">
<div class="torrent-header">
<div class="torrent-title">
<div class="torrent-name">{t.name}</div>
<div class="torrent-title">
<div class="torrent-name">{t.name}</div>
{#if t.type === "youtube"}
<div class="torrent-subtitle">
Source: YouTube
@@ -577,8 +613,15 @@
<div class="torrent-subtitle">
Added: {formatDate(t.added)}
</div>
{:else if t.type === "pornhub"}
<div class="torrent-subtitle">
Source: Rabbit
</div>
<div class="torrent-subtitle">
Added: {formatDate(t.added)}
</div>
{/if}
</div>
</div>
<div style="display:flex; gap:5px;">
{#if t.type !== "youtube"}
<button

View File

@@ -0,0 +1,8 @@
import { writable } from "svelte/store";
const countStore = writable(0);
export const rabbitCount = countStore;
export function setRabbitCount(count) {
countStore.set(Number(count) || 0);
}

View File

@@ -43,6 +43,7 @@ const IMAGE_THUMB_ROOT = path.join(THUMBNAIL_DIR, "images");
const MOVIE_DATA_ROOT = path.join(CACHE_DIR, "movie_data");
const TV_DATA_ROOT = path.join(CACHE_DIR, "tv_data");
const YT_DATA_ROOT = path.join(CACHE_DIR, "yt_data");
const RABBIT_DATA_ROOT = path.join(CACHE_DIR, "rabbit_data");
const MUSIC_EXTENSIONS = new Set([
".mp3",
".m4a",
@@ -61,7 +62,8 @@ for (const dir of [
IMAGE_THUMB_ROOT,
MOVIE_DATA_ROOT,
TV_DATA_ROOT,
YT_DATA_ROOT
YT_DATA_ROOT,
RABBIT_DATA_ROOT
]) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
@@ -695,6 +697,24 @@ function normalizeYoutubeWatchUrl(value) {
}
}
function normalizePornhubUrl(value) {
if (!value || typeof value !== "string") return null;
try {
const urlObj = new URL(value.trim());
if (urlObj.protocol !== "https:") return null;
const host = urlObj.hostname.toLowerCase();
if (host !== "www.pornhub.com" && host !== "pornhub.com") return null;
if (urlObj.pathname !== "/view_video.php") return null;
const viewkey = urlObj.searchParams.get("viewkey");
if (!viewkey) return null;
return `https://www.pornhub.com/view_video.php?viewkey=${encodeURIComponent(
viewkey
)}`;
} catch {
return null;
}
}
function startYoutubeDownload(url) {
const normalized = normalizeYoutubeWatchUrl(url);
if (!normalized) return null;
@@ -739,6 +759,47 @@ function startYoutubeDownload(url) {
return job;
}
function startPornhubDownload(url) {
const normalized = normalizePornhubUrl(url);
if (!normalized) return null;
const viewkey = new URL(normalized).searchParams.get("viewkey");
const folderId = `ph_${viewkey}_${Date.now().toString(36)}`;
const savePath = path.join(DOWNLOAD_DIR, folderId);
fs.mkdirSync(savePath, { recursive: true });
const job = {
id: folderId,
infoHash: folderId,
type: "pornhub",
url: normalized,
videoId: viewkey,
folderId,
savePath,
added: Date.now(),
title: null,
state: "downloading",
progress: 0,
downloaded: 0,
totalBytes: 0,
downloadSpeed: 0,
stages: [],
currentStage: null,
completedBytes: 0,
files: [],
selectedIndex: 0,
thumbnail: null,
process: null,
error: null,
debug: { binary: null, args: null, logs: [] }
};
youtubeJobs.set(job.id, job);
launchPornhubJob(job);
console.log(`▶️ Pornhub indirmesi başlatıldı: ${job.url}`);
broadcastSnapshot();
return job;
}
function appendYoutubeLog(job, line) {
if (!job?.debug) return;
const lines = Array.isArray(job.debug.logs) ? job.debug.logs : [];
@@ -870,13 +931,88 @@ function launchYoutubeJob(job) {
const line = raw.trim();
if (!line) continue;
processYoutubeOutput(job, line);
if (job.type === "pornhub") {
console.log(`[yt-dlp ph:${job.id}] ${line}`);
}
}
};
child.stdout.on("data", handleChunk);
child.stderr.on("data", handleChunk);
child.on("close", (code) => finalizeYoutubeJob(job, code));
child.on("close", (code) => {
if (code !== 0) {
console.warn(`❌ yt-dlp exit code ${code} (${job.type || "youtube"}): ${job.url}`);
}
finalizeYoutubeJob(job, code);
});
child.on("error", (err) => {
job.state = "error";
job.downloadSpeed = 0;
appendYoutubeLog(job, `spawn error: ${err?.message || err}`);
job.error = err?.message || "yt-dlp çalıştırılamadı";
console.error("❌ yt-dlp spawn error:", {
jobId: job.id,
message: err?.message || err,
binary,
args
});
broadcastSnapshot();
});
}
function launchPornhubJob(job) {
const binary = getYtDlpBinary();
const args = [
"-f",
"bestvideo+bestaudio/best",
"--write-thumbnail",
"--convert-thumbnails",
"jpg",
"--write-info-json",
"--concurrent-fragments",
"10",
job.url
];
job.debug = {
binary,
args,
logs: [],
jsRuntime: null,
cookies: null,
extractorArgs: null,
resolution: null,
onlyAudio: false,
format: "bestvideo+bestaudio/best"
};
const child = spawn(binary, args, {
cwd: job.savePath,
env: process.env
});
job.process = child;
const handleChunk = (chunk) => {
const text = chunk.toString();
appendYoutubeLog(job, text);
for (const raw of text.split(/\r?\n/)) {
const line = raw.trim();
if (!line) continue;
processYoutubeOutput(job, line);
if (job.type === "pornhub") {
console.log(`[yt-dlp ph:${job.id}] ${line}`);
}
}
};
child.stdout.on("data", handleChunk);
child.stderr.on("data", handleChunk);
child.on("close", (code) => {
console.log(` yt-dlp kapandı (${job.type || "youtube"}): exit ${code}`);
finalizeYoutubeJob(job, code);
});
child.on("error", (err) => {
job.state = "error";
job.downloadSpeed = 0;
@@ -900,7 +1036,7 @@ function processYoutubeOutput(job, line) {
}
const progressMatch = line.match(
/^\[download\]\s+([\d.]+)%\s+of\s+([\d.]+)\s*([KMGTP]?i?B)(?:\s+at\s+([\d.]+)\s*([KMGTP]?i?B)\/s)?/i
/^\[download\]\s+([\d.]+)%\s+of\s+~?\s*([\d.]+)\s*([KMGTP]?i?B)(?:\s+at\s+([\d.]+)\s*([KMGTP]?i?B)\/s)?/i
);
if (progressMatch) {
updateYoutubeProgress(job, progressMatch);
@@ -964,28 +1100,29 @@ function updateYoutubeProgress(job, match) {
async function finalizeYoutubeJob(job, exitCode) {
job.downloadSpeed = 0;
const tailLines = job.debug?.logs ? job.debug.logs.slice(-8) : [];
const fallbackMedia = findYoutubeMediaFile(job.savePath, Boolean(job.onlyAudio));
if (exitCode !== 0 && !fallbackMedia) {
job.state = "error";
const tail = job.debug?.logs ? job.debug.logs.slice(-8) : [];
job.error = `yt-dlp ${exitCode} kodu ile sonlandı`;
if (tail.length) {
job.error += ` | ${tail.join(" | ")}`;
if (tailLines.length) {
job.error += ` | ${tailLines.join(" | ")}`;
}
console.warn("❌ yt-dlp çıkış kodu hata:", {
jobId: job.id,
exitCode,
binary: job.debug?.binary,
args: job.debug?.args,
lastLines: tail
lastLines: tailLines
});
broadcastSnapshot();
return;
}
if (exitCode !== 0 && fallbackMedia) {
console.warn(
`⚠️ yt-dlp çıkış kodu ${exitCode} ancak medya bulundu, devam ediliyor: ${fallbackMedia}`
);
console.warn(`⚠️ yt-dlp çıkış kodu ${exitCode}, medya bulundu: ${fallbackMedia}`, {
jobId: job.id,
lastLines: tailLines
});
}
try {
@@ -1012,7 +1149,36 @@ async function finalizeYoutubeJob(job, exitCode) {
}
const absMedia = path.join(job.savePath, mediaFile);
const stats = fs.statSync(absMedia);
let mediaPath = absMedia;
if (!fs.existsSync(mediaPath) && mediaFile.endsWith(".temp.mp4")) {
const alt = mediaFile.replace(".temp.mp4", ".mp4");
const altPath = path.join(job.savePath, alt);
if (fs.existsSync(altPath)) {
mediaPath = altPath;
}
}
if (!fs.existsSync(mediaPath)) {
const retry = findYoutubeMediaFile(job.savePath, Boolean(job.onlyAudio));
if (retry && retry !== mediaFile) {
const retryPath = path.join(job.savePath, retry);
if (fs.existsSync(retryPath)) {
mediaPath = retryPath;
}
}
}
if (!fs.existsSync(mediaPath)) {
job.state = "error";
job.error = "Video dosyası bulunamadı";
console.warn("❌ yt-dlp çıktı video bulunamadı (fs):", {
jobId: job.id,
savePath: job.savePath,
fileTried: mediaFile
});
broadcastSnapshot();
return;
}
const stats = fs.statSync(mediaPath);
const mediaInfo = await extractMediaInfo(absMedia).catch(() => null);
const relativeName = mediaFile.replace(/\\/g, "/");
job.files = [
@@ -1032,7 +1198,7 @@ async function finalizeYoutubeJob(job, exitCode) {
const metadataPayload = await writeYoutubeMetadata(
job,
absMedia,
mediaPath,
mediaInfo,
infoJson
);
@@ -1040,6 +1206,28 @@ async function finalizeYoutubeJob(job, exitCode) {
updateYoutubeThumbnail(job, metadataPayload) || metadataPayload;
const mediaType = payloadWithThumb?.type || "video";
const categories = payloadWithThumb?.categories || null;
if (job.type === "pornhub") {
let parsedInfo = null;
if (infoJson) {
try {
parsedInfo = JSON.parse(
fs.readFileSync(path.join(job.savePath, infoJson), "utf-8")
);
} catch (err) {
console.warn("⚠️ Rabbit info.json okunamadı:", err.message);
}
}
writeRabbitMetadata(job, mediaPath, mediaInfo, parsedInfo);
broadcastRabbitCount();
}
// Pornhub tamamlandı işareti
if (job.type === "pornhub") {
try {
fs.writeFileSync(path.join(job.savePath, ".ph_complete"), `${Date.now()}`);
} catch (err) {
console.warn("⚠️ Pornhub tamam işareti yazılamadı:", err.message);
}
}
upsertInfoFile(job.savePath, {
infoHash: job.id,
name: job.title,
@@ -1220,6 +1408,65 @@ function updateYoutubeThumbnail(job, metadataPayload = null) {
return metadataPayload || null;
}
function writeRabbitMetadata(job, absMedia, mediaInfo, infoJson) {
try {
const targetDir = path.join(RABBIT_DATA_ROOT, job.folderId);
fs.mkdirSync(targetDir, { recursive: true });
let thumbnailPath = null;
const thumbs = fs
.readdirSync(job.savePath, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".jpg"));
if (thumbs.length) {
const source = path.join(job.savePath, thumbs[0].name);
const target = path.join(targetDir, "thumbnail.jpg");
fs.copyFileSync(source, target);
thumbnailPath = `/rabbit-data/${job.folderId}/thumbnail.jpg`;
}
const payload = {
id: job.folderId,
title: job.title,
url: job.url,
added: job.added,
folderId: job.folderId,
file: job.files?.[0]?.name || path.basename(absMedia),
size: mediaInfo?.format?.size || null,
mediaInfo,
thumbnail: thumbnailPath,
source: "pornhub",
infoJson: infoJson || null
};
fs.writeFileSync(
path.join(targetDir, "metadata.json"),
JSON.stringify(payload, null, 2),
"utf-8"
);
return payload;
} catch (err) {
console.warn("⚠️ Rabbit metadata yazılamadı:", err.message);
return null;
}
}
function countRabbitItems() {
try {
const entries = fs
.readdirSync(RABBIT_DATA_ROOT, { withFileTypes: true })
.filter((d) => d.isDirectory());
return entries.length;
} catch (err) {
console.warn("⚠️ Rabbit sayısı okunamadı:", err.message);
return 0;
}
}
function broadcastRabbitCount() {
if (!wss) return;
const count = countRabbitItems();
broadcastJson(wss, { type: "rabbitCount", count });
}
function removeYoutubeJob(jobId, { removeFiles = true } = {}) {
const job = youtubeJobs.get(jobId);
if (!job) return false;
@@ -1264,7 +1511,7 @@ function youtubeSnapshot(job) {
}));
return {
infoHash: job.id,
type: "youtube",
type: job.type || "youtube",
name: job.title || job.url,
progress: Math.min(1, job.progress || 0),
downloaded: job.downloaded || 0,
@@ -1953,6 +2200,18 @@ function resolveYoutubeDataAbsolute(relPath) {
return resolved;
}
function resolveRabbitDataAbsolute(relPath) {
const normalized = sanitizeRelative(relPath);
const resolved = path.resolve(RABBIT_DATA_ROOT, normalized);
if (
resolved !== RABBIT_DATA_ROOT &&
!resolved.startsWith(RABBIT_DATA_ROOT + path.sep)
) {
return null;
}
return resolved;
}
function removeAllThumbnailsForRoot(rootFolder) {
const safe = sanitizeRelative(rootFolder);
if (!safe) return;
@@ -4675,6 +4934,13 @@ app.get("/yt-data/:path(*)", requireAuth, (req, res) => {
return serveCachedFile(req, res, fullPath, { maxAgeSeconds: 60 * 60 * 24 });
});
app.get("/rabbit-data/:path(*)", requireAuth, (req, res) => {
const relPath = req.params.path || "";
const fullPath = resolveRabbitDataAbsolute(relPath);
if (!fullPath) return res.status(400).send("Geçersiz rabbit data yolu");
return serveCachedFile(req, res, fullPath, { maxAgeSeconds: 60 * 60 * 24 });
});
app.get("/tv-data/:path(*)", requireAuth, (req, res) => {
const relPath = req.params.path || "";
const fullPath = resolveTvDataAbsolute(relPath);
@@ -6009,6 +6275,59 @@ app.post("/api/youtube/download", requireAuth, async (req, res) => {
}
});
app.post("/api/pornhub/download", requireAuth, async (req, res) => {
try {
const rawUrl = req.body?.url;
const job = startPornhubDownload(rawUrl);
if (!job) {
return res
.status(400)
.json({ ok: false, error: "Geçerli bir Pornhub URL'si gerekli." });
}
res.json({ ok: true, jobId: job.id });
} catch (err) {
res.status(500).json({
ok: false,
error: err?.message || "Pornhub indirimi başarısız oldu."
});
}
});
app.get("/api/rabbit", requireAuth, (req, res) => {
try {
const entries = fs
.readdirSync(RABBIT_DATA_ROOT, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
const items = [];
for (const folder of entries) {
const metaPath = path.join(RABBIT_DATA_ROOT, folder, "metadata.json");
if (!fs.existsSync(metaPath)) continue;
try {
const data = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
items.push({
id: data.id || folder,
title: data.title || folder,
url: data.url || null,
added: data.added || null,
thumbnail: data.thumbnail || null,
file: data.file || null,
size: data.size || null,
mediaInfo: data.mediaInfo || null,
source: data.source || "pornhub"
});
} catch (err) {
console.warn(`⚠️ Rabbit metadata okunamadı (${folder}): ${err.message}`);
}
}
items.sort((a, b) => (b.added || 0) - (a.added || 0));
res.json({ ok: true, items, count: items.length });
} catch (err) {
console.error("❌ Rabbit listesi alınamadı:", err.message);
res.status(500).json({ ok: false, error: "Rabbit listesi alınamadı." });
}
});
// --- 🎫 YouTube cookies yönetimi ---
app.get("/api/youtube/cookies", requireAuth, (req, res) => {
try {
@@ -6955,6 +7274,12 @@ wss.on("connection", (ws) => {
ws.send(JSON.stringify({ type: "progress", torrents: snapshot() }));
// Bağlantı kurulduğunda disk space bilgisi gönder
broadcastDiskSpace();
try {
const count = countRabbitItems();
ws.send(JSON.stringify({ type: "rabbitCount", count }));
} catch (err) {
console.warn("⚠️ Rabbit count gönderilemedi:", err.message);
}
});
// --- ⏱️ Her 2 saniyede bir aktif torrent durumu yayınla ---