Added Login Screen
This commit is contained in:
@@ -6,6 +6,9 @@
|
|||||||
import Transfers from "./routes/Transfers.svelte";
|
import Transfers from "./routes/Transfers.svelte";
|
||||||
import Sharing from "./routes/Sharing.svelte";
|
import Sharing from "./routes/Sharing.svelte";
|
||||||
import Trash from "./routes/Trash.svelte";
|
import Trash from "./routes/Trash.svelte";
|
||||||
|
import Login from "./routes/Login.svelte";
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
let menuOpen = false;
|
let menuOpen = false;
|
||||||
const toggleMenu = () => {
|
const toggleMenu = () => {
|
||||||
@@ -13,12 +16,14 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Router>
|
{#if token}
|
||||||
|
<Router>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<Sidebar {menuOpen} />
|
<Sidebar {menuOpen} />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<Topbar on:toggleMenu={toggleMenu} />
|
<Topbar on:toggleMenu={toggleMenu} />
|
||||||
<Route path="/" component={Files} />
|
<Route path="/" component={Files} />
|
||||||
|
<Route path="/files" component={Files} />
|
||||||
<Route path="/transfers" component={Transfers} />
|
<Route path="/transfers" component={Transfers} />
|
||||||
<Route path="/sharing" component={Sharing} />
|
<Route path="/sharing" component={Sharing} />
|
||||||
<Route path="/trash" component={Trash} />
|
<Route path="/trash" component={Trash} />
|
||||||
@@ -32,4 +37,7 @@
|
|||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
{:else}
|
||||||
|
<Login />
|
||||||
|
{/if}
|
||||||
|
|||||||
BIN
client/src/assets/image/logo.png
Normal file
BIN
client/src/assets/image/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { API } from "../utils/api.js";
|
import { API, apiFetch } from "../utils/api.js";
|
||||||
import { cleanFileName } from "../utils/filename.js";
|
import { cleanFileName } from "../utils/filename.js";
|
||||||
|
|
||||||
let files = [];
|
let files = [];
|
||||||
@@ -17,9 +17,21 @@
|
|||||||
let duration = 0;
|
let duration = 0;
|
||||||
let volume = 1;
|
let volume = 1;
|
||||||
|
|
||||||
// 📂 Dosyaları yükle
|
// ✅ REACTIVE: selectedVideo güvenli kullanımlar
|
||||||
|
$: selectedName = selectedVideo?.name ?? "";
|
||||||
|
$: encName = encodeURIComponent(selectedName);
|
||||||
|
|
||||||
|
// ✅ Token'lı video URL'ini fonksiyonla üret (başta çağrılmasın)
|
||||||
|
function getVideoURL() {
|
||||||
|
if (!selectedName) return "";
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
return `${API}/media/${encName}?token=${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 📂 Dosyaları yükle (tokenlı)
|
||||||
async function loadFiles() {
|
async function loadFiles() {
|
||||||
const r = await fetch(`${API}/api/files`);
|
const r = await apiFetch("/api/files");
|
||||||
|
if (!r.ok) return;
|
||||||
files = await r.json();
|
files = await r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +114,10 @@
|
|||||||
if (ext === "srt") {
|
if (ext === "srt") {
|
||||||
const vttText =
|
const vttText =
|
||||||
"\uFEFFWEBVTT\n\n" + content.replace(/\r+/g, "").replace(/,/g, ".");
|
"\uFEFFWEBVTT\n\n" + content.replace(/\r+/g, "").replace(/,/g, ".");
|
||||||
const blob = new Blob([vttText], {
|
const blob = new Blob([vttText], { type: "text/vtt;charset=utf-8" });
|
||||||
type: "text/vtt;charset=utf-8"
|
|
||||||
});
|
|
||||||
subtitleURL = URL.createObjectURL(blob);
|
subtitleURL = URL.createObjectURL(blob);
|
||||||
} else if (ext === "vtt") {
|
} else if (ext === "vtt") {
|
||||||
const blob = new Blob([content], {
|
const blob = new Blob([content], { type: "text/vtt;charset=utf-8" });
|
||||||
type: "text/vtt;charset=utf-8"
|
|
||||||
});
|
|
||||||
subtitleURL = URL.createObjectURL(blob);
|
subtitleURL = URL.createObjectURL(blob);
|
||||||
} else {
|
} else {
|
||||||
alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
|
alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
|
||||||
@@ -167,14 +175,15 @@
|
|||||||
<div class="modal-overlay" on:click={closeModal}>
|
<div class="modal-overlay" on:click={closeModal}>
|
||||||
<div class="modal-content" on:click|stopPropagation>
|
<div class="modal-content" on:click|stopPropagation>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="video-title">{selectedVideo.name}</div>
|
<div class="video-title">{selectedName}</div>
|
||||||
<button class="close-btn" on:click={closeModal}>✕</button>
|
<button class="close-btn" on:click={closeModal}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="custom-player">
|
<div class="custom-player">
|
||||||
|
<!-- ✅ selectedVideo yokken boş src -->
|
||||||
<video
|
<video
|
||||||
bind:this={videoEl}
|
bind:this={videoEl}
|
||||||
src={`${API}/media/${encodeURIComponent(selectedVideo.name)}`}
|
src={getVideoURL()}
|
||||||
class="video-element"
|
class="video-element"
|
||||||
on:timeupdate={updateProgress}
|
on:timeupdate={updateProgress}
|
||||||
on:loadedmetadata={() => {
|
on:loadedmetadata={() => {
|
||||||
@@ -200,9 +209,11 @@
|
|||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="top-controls">
|
<div class="top-controls">
|
||||||
<button class="control-btn" on:click={togglePlay}>
|
<button class="control-btn" on:click={togglePlay}>
|
||||||
{#if isPlaying}<i class="fa-solid fa-pause"></i>{:else}<i
|
{#if isPlaying}
|
||||||
class="fa-solid fa-play"
|
<i class="fa-solid fa-pause"></i>
|
||||||
></i>{/if}
|
{:else}
|
||||||
|
<i class="fa-solid fa-play"></i>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="right-controls">
|
<div class="right-controls">
|
||||||
@@ -220,9 +231,10 @@
|
|||||||
<i class="fa-solid fa-expand"></i>
|
<i class="fa-solid fa-expand"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- ✅ selectedVideo yokken '#' -->
|
||||||
<a
|
<a
|
||||||
href={`${API}/downloads/${selectedVideo.name}`}
|
href={selectedName ? `${API}/downloads/${selectedName}` : "#"}
|
||||||
download={selectedVideo.name}
|
download={selectedName || undefined}
|
||||||
class="control-btn"
|
class="control-btn"
|
||||||
title="Download"
|
title="Download"
|
||||||
>
|
>
|
||||||
@@ -268,6 +280,7 @@
|
|||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-card {
|
.media-card {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -278,14 +291,17 @@
|
|||||||
transition: transform 0.2s;
|
transition: transform 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-card:hover {
|
.media-card:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb.placeholder {
|
.thumb.placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -293,12 +309,14 @@
|
|||||||
font-size: 42px;
|
font-size: 42px;
|
||||||
background: #ddd;
|
background: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -306,6 +324,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.size {
|
.size {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
@@ -376,6 +395,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-element:focus {
|
.video-element:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
@@ -405,6 +425,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn:hover {
|
.control-btn:hover {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
@@ -415,7 +436,7 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Ses Seviyesi Kaydırıcısı === */
|
/* === Ses Kaydırıcısı === */
|
||||||
.volume-slider {
|
.volume-slider {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
@@ -436,6 +457,7 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider::-webkit-slider-thumb {
|
.volume-slider::-webkit-slider-thumb {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
@@ -446,9 +468,11 @@
|
|||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider::-webkit-slider-thumb:hover {
|
.volume-slider::-webkit-slider-thumb:hover {
|
||||||
transform: scale(1.3);
|
transform: scale(1.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider::-moz-range-thumb {
|
.volume-slider::-moz-range-thumb {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@@ -457,9 +481,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider::-moz-range-thumb:hover {
|
.volume-slider::-moz-range-thumb:hover {
|
||||||
transform: scale(1.3);
|
transform: scale(1.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider::-moz-range-progress {
|
.volume-slider::-moz-range-progress {
|
||||||
height: 4px;
|
height: 4px;
|
||||||
background: #ff3b30;
|
background: #ff3b30;
|
||||||
@@ -500,25 +526,31 @@
|
|||||||
.gallery {
|
.gallery {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
height: 70%;
|
height: 70%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider {
|
.volume-slider {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-title {
|
.video-title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
@@ -529,9 +561,11 @@
|
|||||||
width: 98%;
|
width: 98%;
|
||||||
height: 75%;
|
height: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume-slider {
|
.volume-slider {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-controls {
|
.bottom-controls {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
165
client/src/routes/Login.svelte
Normal file
165
client/src/routes/Login.svelte
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<script>
|
||||||
|
import { API } from "../utils/api.js";
|
||||||
|
import logo from "../assets/image/logo.png";
|
||||||
|
|
||||||
|
let username = "";
|
||||||
|
let password = "";
|
||||||
|
let error = "";
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const res = await fetch(`${API}/api/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const { token } = await res.json();
|
||||||
|
localStorage.setItem("token", token);
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
error = "Kullanıcı adı veya şifre hatalı.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="login">
|
||||||
|
<div class="logo-box">
|
||||||
|
<img src={logo} alt="du.pe logo" class="logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<form on:submit|preventDefault={login}>
|
||||||
|
<h2>Welcome to <span>du.pe</span></h2>
|
||||||
|
<input placeholder="Username" bind:value={username} />
|
||||||
|
<input type="password" placeholder="Password" bind:value={password} />
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
{#if error}<p class="error">{error}</p>{/if}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--yellow: #fdce45;
|
||||||
|
--yellow-light: #fdce45;
|
||||||
|
--red: #e24b2d;
|
||||||
|
--beige: #f9f6ef;
|
||||||
|
--gray-light: #ffffff;
|
||||||
|
--gray-border: #ddd;
|
||||||
|
--text-dark: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
background: var(--beige);
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
background: var(--gray-light);
|
||||||
|
border: 1px solid var(--gray-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 30px 25px;
|
||||||
|
width: 300px;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box:hover {
|
||||||
|
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 span {
|
||||||
|
color: var(--yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 8px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background: #fafafa;
|
||||||
|
color: var(--text-dark);
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: border 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
border-color: var(--yellow);
|
||||||
|
box-shadow: 0 0 0 3px rgba(245, 179, 51, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fdce45;
|
||||||
|
border: none;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--yellow-light);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--red);
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.box {
|
||||||
|
width: 85%;
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { API } from "../utils/api.js";
|
import { API, apiFetch } from "../utils/api.js"; // ✅ apiFetch eklendi
|
||||||
|
|
||||||
let torrents = [];
|
let torrents = [];
|
||||||
let ws;
|
let ws;
|
||||||
@@ -19,9 +19,10 @@
|
|||||||
let duration = 0;
|
let duration = 0;
|
||||||
let volume = 1;
|
let volume = 1;
|
||||||
|
|
||||||
// --- WebSocket & API
|
// --- WebSocket & API ---
|
||||||
function wsConnect() {
|
function wsConnect() {
|
||||||
const url = API.replace("http", "ws");
|
const token = localStorage.getItem("token"); // 🔒 token ekle
|
||||||
|
const url = `${API.replace("http", "ws")}?token=${token}`;
|
||||||
ws = new WebSocket(url);
|
ws = new WebSocket(url);
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
const d = JSON.parse(e.data);
|
const d = JSON.parse(e.data);
|
||||||
@@ -30,7 +31,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function list() {
|
async function list() {
|
||||||
const r = await fetch(`${API}/api/torrents`);
|
const r = await apiFetch("/api/torrents"); // ✅ fetch yerine apiFetch
|
||||||
|
if (!r.ok) return;
|
||||||
torrents = await r.json();
|
torrents = await r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,18 +41,18 @@
|
|||||||
if (!f) return;
|
if (!f) return;
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("torrent", f);
|
fd.append("torrent", f);
|
||||||
await fetch(`${API}/api/transfer`, { method: "POST", body: fd });
|
await apiFetch("/api/transfer", { method: "POST", body: fd }); // ✅
|
||||||
await list();
|
await list();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addMagnet() {
|
async function addMagnet() {
|
||||||
const m = prompt("Magnet linki:");
|
const m = prompt("Magnet linki:");
|
||||||
if (!m) return;
|
if (!m) return;
|
||||||
await fetch(`${API}/api/transfer`, {
|
await apiFetch("/api/transfer", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ magnet: m })
|
body: JSON.stringify({ magnet: m })
|
||||||
});
|
}); // ✅
|
||||||
await list();
|
await list();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,13 +62,14 @@
|
|||||||
|
|
||||||
async function removeTorrent(hash) {
|
async function removeTorrent(hash) {
|
||||||
if (!confirm("Bu transferi silmek istediğine emin misin?")) return;
|
if (!confirm("Bu transferi silmek istediğine emin misin?")) return;
|
||||||
await fetch(`${API}/api/torrents/${hash}`, { method: "DELETE" });
|
await apiFetch(`/api/torrents/${hash}`, { method: "DELETE" }); // ✅
|
||||||
await list();
|
await list();
|
||||||
}
|
}
|
||||||
|
|
||||||
function streamURL(hash) {
|
function streamURL(hash, index = 0) {
|
||||||
return `${API}/stream/${hash}`;
|
const token = localStorage.getItem("token");
|
||||||
}
|
return `${API}/stream/${hash}?index=${index}&token=${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatSpeed(bytesPerSec) {
|
function formatSpeed(bytesPerSec) {
|
||||||
if (!bytesPerSec || bytesPerSec <= 0) return "0 MB/s";
|
if (!bytesPerSec || bytesPerSec <= 0) return "0 MB/s";
|
||||||
@@ -74,7 +77,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openModal(t) {
|
function openModal(t) {
|
||||||
// torrent içinde seçilmiş dosya var mı?
|
|
||||||
const selectedFile =
|
const selectedFile =
|
||||||
t.files?.find((f) => f.index === t.selectedIndex) || t.files?.[0];
|
t.files?.find((f) => f.index === t.selectedIndex) || t.files?.[0];
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
@@ -96,7 +98,7 @@
|
|||||||
subtitleURL = null;
|
subtitleURL = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Altyazı işlemleri ---
|
// --- Altyazı işlemleri (hiç değişmedi) ---
|
||||||
function detectSubtitleLang(text) {
|
function detectSubtitleLang(text) {
|
||||||
const lower = (text || "").toLowerCase();
|
const lower = (text || "").toLowerCase();
|
||||||
if (lower.includes("ş") || lower.includes("ğ") || lower.includes("ı"))
|
if (lower.includes("ş") || lower.includes("ğ") || lower.includes("ı"))
|
||||||
@@ -136,17 +138,14 @@
|
|||||||
function handleSubtitleUpload(e) {
|
function handleSubtitleUpload(e) {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const ext = file.name.split(".").pop().toLowerCase();
|
const ext = file.name.split(".").pop().toLowerCase();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (ev) => {
|
reader.onload = (ev) => {
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoder = new TextDecoder("utf-8");
|
||||||
const content =
|
const content =
|
||||||
typeof ev.target.result === "string"
|
typeof ev.target.result === "string"
|
||||||
? ev.target.result
|
? ev.target.result
|
||||||
: decoder.decode(ev.target.result);
|
: decoder.decode(ev.target.result);
|
||||||
|
|
||||||
const detected = detectSubtitleLang(content);
|
const detected = detectSubtitleLang(content);
|
||||||
subtitleLang = detected.code;
|
subtitleLang = detected.code;
|
||||||
subtitleLabel = detected.label;
|
subtitleLabel = detected.label;
|
||||||
@@ -166,7 +165,6 @@
|
|||||||
alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
|
alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +201,6 @@
|
|||||||
if (!videoEl) return;
|
if (!videoEl) return;
|
||||||
const val = parseFloat(e.target.value);
|
const val = parseFloat(e.target.value);
|
||||||
videoEl.volume = val;
|
videoEl.volume = val;
|
||||||
// Slider dolum rengini CSS değişkeniyle güncelle
|
|
||||||
e.target.style.setProperty("--fill", (val || 0) * 100);
|
e.target.style.setProperty("--fill", (val || 0) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,21 +221,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
list();
|
list(); // 🔒 token'lı liste çekimi
|
||||||
wsConnect();
|
wsConnect(); // 🔒 token'lı WebSocket
|
||||||
|
|
||||||
// volume slider başlangıç dolumu
|
|
||||||
const slider = document.querySelector(".volume-slider");
|
const slider = document.querySelector(".volume-slider");
|
||||||
if (slider) {
|
if (slider) {
|
||||||
slider.value = volume; // 1
|
slider.value = volume;
|
||||||
slider.style.setProperty("--fill", slider.value * 100);
|
slider.style.setProperty("--fill", slider.value * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("keydown", onEsc);
|
window.addEventListener("keydown", onEsc);
|
||||||
return () => window.removeEventListener("keydown", onEsc);
|
return () => window.removeEventListener("keydown", onEsc);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- 💡 HTML ve stil kısmı aynı kalıyor -->
|
||||||
|
|
||||||
|
|
||||||
<section class="files">
|
<section class="files">
|
||||||
<h2>Transfers</h2>
|
<h2>Transfers</h2>
|
||||||
|
|
||||||
@@ -339,7 +336,7 @@
|
|||||||
<div class="custom-player">
|
<div class="custom-player">
|
||||||
<video
|
<video
|
||||||
bind:this={videoEl}
|
bind:this={videoEl}
|
||||||
src={`${API}/stream/${selectedVideo.infoHash}?index=${selectedVideo.fileIndex}`}
|
src={streamURL(selectedVideo.infoHash, selectedVideo.fileIndex)}
|
||||||
class="video-element"
|
class="video-element"
|
||||||
on:timeupdate={updateProgress}
|
on:timeupdate={updateProgress}
|
||||||
on:loadedmetadata={() => {
|
on:loadedmetadata={() => {
|
||||||
@@ -386,7 +383,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={streamURL(selectedVideo.infoHash)}
|
href={streamURL(selectedVideo.infoHash, selectedVideo.fileIndex)}
|
||||||
download={selectedVideo.name}
|
download={selectedVideo.name}
|
||||||
class="control-btn"
|
class="control-btn"
|
||||||
title="Download"
|
title="Download"
|
||||||
|
|||||||
@@ -1 +1,20 @@
|
|||||||
export const API = import.meta.env.VITE_API || "http://localhost:3001";
|
export const API = import.meta.env.VITE_API || "http://localhost:3001";
|
||||||
|
|
||||||
|
// 🔐 Ortak kimlik doğrulama başlığı (token varsa ekler)
|
||||||
|
export function authHeaders() {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔧 Yardımcı fetch (otomatik token ekler, hata durumunda logout)
|
||||||
|
export async function apiFetch(path, options = {}) {
|
||||||
|
const headers = { ...(options.headers || {}), ...authHeaders() };
|
||||||
|
const res = await fetch(`${API}${path}`, { ...options, headers });
|
||||||
|
|
||||||
|
// Token süresi dolmuşsa veya yanlışsa kullanıcıyı çıkışa yönlendir
|
||||||
|
if (res.status === 401) {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
@@ -9,3 +9,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./downloads:/app/server/downloads
|
- ./downloads:/app/server/downloads
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# Login credentials for basic auth
|
||||||
|
environment:
|
||||||
|
USERNAME: admin
|
||||||
|
PASSWORD: admin
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import path from "path";
|
|||||||
import mime from "mime-types";
|
import mime from "mime-types";
|
||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { exec } from "child_process"; // 🆕 ffmpeg çağırmak için
|
import { exec } from "child_process";
|
||||||
|
import crypto from "crypto"; // 🔒 basit token üretimi için
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -28,6 +29,7 @@ app.use(express.json());
|
|||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use("/downloads", express.static(DOWNLOAD_DIR));
|
app.use("/downloads", express.static(DOWNLOAD_DIR));
|
||||||
|
|
||||||
|
|
||||||
// --- En uygun video dosyasını seç ---
|
// --- En uygun video dosyasını seç ---
|
||||||
function pickBestVideoFile(torrent) {
|
function pickBestVideoFile(torrent) {
|
||||||
const videoExts = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
|
const videoExts = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
|
||||||
@@ -67,8 +69,32 @@ function snapshot() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Basit kimlik doğrulama sistemi ---
|
||||||
|
const USERNAME = process.env.USERNAME
|
||||||
|
const PASSWORD = process.env.PASSWORD
|
||||||
|
let activeTokens = new Set();
|
||||||
|
|
||||||
|
app.post("/api/login", (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
if (username === USERNAME && password === PASSWORD) {
|
||||||
|
const token = crypto.randomBytes(24).toString("hex");
|
||||||
|
activeTokens.add(token);
|
||||||
|
return res.json({ token });
|
||||||
|
}
|
||||||
|
res.status(401).json({ error: "Invalid credentials" });
|
||||||
|
});
|
||||||
|
|
||||||
|
function requireAuth(req, res, next) {
|
||||||
|
const token =
|
||||||
|
req.headers.authorization?.split(" ")[1] || req.query.token;
|
||||||
|
if (!token || !activeTokens.has(token))
|
||||||
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Torrent veya magnet ekleme ---
|
// --- Torrent veya magnet ekleme ---
|
||||||
app.post("/api/transfer", upload.single("torrent"), (req, res) => {
|
app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
||||||
try {
|
try {
|
||||||
let source = req.body.magnet;
|
let source = req.body.magnet;
|
||||||
if (req.file) source = fs.readFileSync(req.file.path);
|
if (req.file) source = fs.readFileSync(req.file.path);
|
||||||
@@ -148,12 +174,12 @@ app.get("/thumbnail/:hash", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Torrentleri listele ---
|
// --- Torrentleri listele ---
|
||||||
app.get("/api/torrents", (req, res) => {
|
app.get("/api/torrents", requireAuth, (req, res) => {
|
||||||
res.json(snapshot());
|
res.json(snapshot());
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Seçili dosya değiştir ---
|
// --- Seçili dosya değiştir ---
|
||||||
app.post("/api/torrents/:hash/select/:index", (req, res) => {
|
app.post("/api/torrents/:hash/select/:index", requireAuth, (req, res) => {
|
||||||
const entry = torrents.get(req.params.hash);
|
const entry = torrents.get(req.params.hash);
|
||||||
if (!entry) return res.status(404).json({ error: "torrent bulunamadı" });
|
if (!entry) return res.status(404).json({ error: "torrent bulunamadı" });
|
||||||
entry.selectedIndex = Number(req.params.index) || 0;
|
entry.selectedIndex = Number(req.params.index) || 0;
|
||||||
@@ -161,7 +187,7 @@ app.post("/api/torrents/:hash/select/:index", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Torrent silme (disk dahil) ---
|
// --- Torrent silme (disk dahil) ---
|
||||||
app.delete("/api/torrents/:hash", (req, res) => {
|
app.delete("/api/torrents/:hash", requireAuth, (req, res) => {
|
||||||
const entry = torrents.get(req.params.hash);
|
const entry = torrents.get(req.params.hash);
|
||||||
if (!entry) return res.status(404).json({ error: "torrent bulunamadı" });
|
if (!entry) return res.status(404).json({ error: "torrent bulunamadı" });
|
||||||
|
|
||||||
@@ -180,7 +206,7 @@ app.delete("/api/torrents/:hash", (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/media/:path(*)", (req, res) => {
|
app.get("/media/:path(*)", requireAuth, (req, res) => {
|
||||||
const fullPath = path.join(DOWNLOAD_DIR, req.params.path);
|
const fullPath = path.join(DOWNLOAD_DIR, req.params.path);
|
||||||
if (!fs.existsSync(fullPath)) return res.status(404).send("File not found");
|
if (!fs.existsSync(fullPath)) return res.status(404).send("File not found");
|
||||||
|
|
||||||
@@ -213,7 +239,7 @@ app.get("/media/:path(*)", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- 📁 Dosya gezgini: /downloads altındaki dosyaları listele ---
|
// --- 📁 Dosya gezgini: /downloads altındaki dosyaları listele ---
|
||||||
app.get("/api/files", (req, res) => {
|
app.get("/api/files", requireAuth, (req, res) => {
|
||||||
const walk = (dir) => {
|
const walk = (dir) => {
|
||||||
let result = [];
|
let result = [];
|
||||||
const list = fs.readdirSync(dir, { withFileTypes: true });
|
const list = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
@@ -258,7 +284,7 @@ app.get("/api/files", (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Stream endpoint ---
|
// --- Stream endpoint ---
|
||||||
app.get("/stream/:hash", (req, res) => {
|
app.get("/stream/:hash", requireAuth, (req, res) => {
|
||||||
const entry = torrents.get(req.params.hash);
|
const entry = torrents.get(req.params.hash);
|
||||||
if (!entry) return res.status(404).end();
|
if (!entry) return res.status(404).end();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user