Grid/list seçim modunu ekle, meta bilgileri göster ve seçili öğeler için toplu silme butonu ekle.
This commit is contained in:
@@ -42,7 +42,19 @@
|
|||||||
|
|
||||||
<!-- Sidebar dışına tıklayınca kapanma -->
|
<!-- Sidebar dışına tıklayınca kapanma -->
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
<div class="backdrop show" on:click={closeSidebar}></div>
|
<div
|
||||||
|
class="backdrop show"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
aria-label="Menüyü kapat"
|
||||||
|
on:click={closeSidebar}
|
||||||
|
on:keydown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
closeSidebar();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,3 +8,4 @@ nfo
|
|||||||
srt
|
srt
|
||||||
txt
|
txt
|
||||||
vtt
|
vtt
|
||||||
|
info.json
|
||||||
|
|||||||
133
server/server.js
133
server/server.js
@@ -38,6 +38,7 @@ for (const dir of [THUMBNAIL_DIR, VIDEO_THUMB_ROOT, IMAGE_THUMB_ROOT]) {
|
|||||||
const VIDEO_THUMBNAIL_TIME = process.env.VIDEO_THUMBNAIL_TIME || "00:00:05";
|
const VIDEO_THUMBNAIL_TIME = process.env.VIDEO_THUMBNAIL_TIME || "00:00:05";
|
||||||
const VIDEO_EXTS = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
|
const VIDEO_EXTS = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
|
||||||
const generatingThumbnails = new Set();
|
const generatingThumbnails = new Set();
|
||||||
|
const INFO_FILENAME = "info.json";
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -59,6 +60,70 @@ function ensureDirForFile(filePath) {
|
|||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function infoFilePath(savePath) {
|
||||||
|
return path.join(savePath, INFO_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInfoFile(savePath) {
|
||||||
|
const target = infoFilePath(savePath);
|
||||||
|
if (!fs.existsSync(target)) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(target, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ info.json okunamadı (${target}): ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertInfoFile(savePath, partial) {
|
||||||
|
const target = infoFilePath(savePath);
|
||||||
|
try {
|
||||||
|
ensureDirForFile(target);
|
||||||
|
let current = {};
|
||||||
|
if (fs.existsSync(target)) {
|
||||||
|
try {
|
||||||
|
current = JSON.parse(fs.readFileSync(target, "utf-8")) || {};
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ info.json parse edilemedi (${target}): ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const next = {
|
||||||
|
...current,
|
||||||
|
...partial,
|
||||||
|
updatedAt: timestamp
|
||||||
|
};
|
||||||
|
if (!next.createdAt) {
|
||||||
|
next.createdAt =
|
||||||
|
current.createdAt ?? partial?.createdAt ?? timestamp;
|
||||||
|
}
|
||||||
|
if (!next.added && partial?.added) {
|
||||||
|
next.added = partial.added;
|
||||||
|
}
|
||||||
|
if (!next.folder) {
|
||||||
|
next.folder = path.basename(savePath);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(target, JSON.stringify(next, null, 2), "utf-8");
|
||||||
|
return next;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ info.json yazılamadı (${target}): ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInfoForRoot(rootFolder) {
|
||||||
|
const safe = sanitizeRelative(rootFolder);
|
||||||
|
if (!safe) return null;
|
||||||
|
const target = path.join(DOWNLOAD_DIR, safe, INFO_FILENAME);
|
||||||
|
if (!fs.existsSync(target)) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(target, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ info.json okunamadı (${target}): ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeRelative(relPath) {
|
function sanitizeRelative(relPath) {
|
||||||
return relPath.replace(/^[\\/]+/, "");
|
return relPath.replace(/^[\\/]+/, "");
|
||||||
}
|
}
|
||||||
@@ -181,14 +246,28 @@ function cleanupEmptyDirs(startDir) {
|
|||||||
while (
|
while (
|
||||||
dir &&
|
dir &&
|
||||||
dir.startsWith(THUMBNAIL_DIR) &&
|
dir.startsWith(THUMBNAIL_DIR) &&
|
||||||
fs.existsSync(dir) &&
|
fs.existsSync(dir)
|
||||||
fs.lstatSync(dir).isDirectory()
|
|
||||||
) {
|
) {
|
||||||
|
try {
|
||||||
|
const stat = fs.lstatSync(dir);
|
||||||
|
if (!stat.isDirectory()) break;
|
||||||
const entries = fs.readdirSync(dir);
|
const entries = fs.readdirSync(dir);
|
||||||
if (entries.length > 0) break;
|
if (entries.length > 0) break;
|
||||||
fs.rmdirSync(dir);
|
fs.rmdirSync(dir);
|
||||||
dir = path.dirname(dir);
|
} catch (err) {
|
||||||
if (dir === THUMBNAIL_DIR || dir === path.dirname(THUMBNAIL_DIR)) break;
|
console.warn(`⚠️ Thumbnail klasörü temizlenemedi (${dir}): ${err.message}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const parent = path.dirname(dir);
|
||||||
|
if (
|
||||||
|
!parent ||
|
||||||
|
parent === dir ||
|
||||||
|
parent.length < THUMBNAIL_DIR.length ||
|
||||||
|
parent === THUMBNAIL_DIR
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dir = parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +393,16 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
|||||||
savePath,
|
savePath,
|
||||||
added
|
added
|
||||||
});
|
});
|
||||||
|
const rootFolder = path.basename(savePath);
|
||||||
|
upsertInfoFile(savePath, {
|
||||||
|
infoHash: torrent.infoHash,
|
||||||
|
name: torrent.name,
|
||||||
|
tracker: torrent.announce?.[0] || null,
|
||||||
|
added,
|
||||||
|
createdAt: added,
|
||||||
|
folder: rootFolder
|
||||||
|
});
|
||||||
|
broadcastFileUpdate(rootFolder);
|
||||||
res.json({
|
res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
@@ -362,6 +451,13 @@ app.post("/api/transfer", requireAuth, upload.single("torrent"), (req, res) => {
|
|||||||
console.warn("⚠️ Eski thumbnail klasörü temizlenemedi:", err.message);
|
console.warn("⚠️ Eski thumbnail klasörü temizlenemedi:", err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upsertInfoFile(entry.savePath, {
|
||||||
|
completedAt: Date.now(),
|
||||||
|
totalBytes: torrent.downloaded,
|
||||||
|
fileCount: torrent.files.length
|
||||||
|
});
|
||||||
|
broadcastFileUpdate(rootFolder);
|
||||||
|
|
||||||
broadcastSnapshot();
|
broadcastSnapshot();
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -546,6 +642,16 @@ app.get("/api/files", requireAuth, (req, res) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const infoCache = new Map();
|
||||||
|
const getInfo = (relPath) => {
|
||||||
|
const root = rootFromRelPath(relPath);
|
||||||
|
if (!root) return null;
|
||||||
|
if (!infoCache.has(root)) {
|
||||||
|
infoCache.set(root, readInfoForRoot(root));
|
||||||
|
}
|
||||||
|
return infoCache.get(root);
|
||||||
|
};
|
||||||
|
|
||||||
// --- 📁 Klasörleri dolaş ---
|
// --- 📁 Klasörleri dolaş ---
|
||||||
const walk = (dir) => {
|
const walk = (dir) => {
|
||||||
let result = [];
|
let result = [];
|
||||||
@@ -561,6 +667,8 @@ app.get("/api/files", requireAuth, (req, res) => {
|
|||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
result = result.concat(walk(full));
|
result = result.concat(walk(full));
|
||||||
} else {
|
} else {
|
||||||
|
if (entry.name.toLowerCase() === INFO_FILENAME) continue;
|
||||||
|
|
||||||
const size = fs.statSync(full).size;
|
const size = fs.statSync(full).size;
|
||||||
const type = mime.lookup(full) || "application/octet-stream";
|
const type = mime.lookup(full) || "application/octet-stream";
|
||||||
|
|
||||||
@@ -588,12 +696,27 @@ app.get("/api/files", requireAuth, (req, res) => {
|
|||||||
else queueImageThumbnail(full, safeRel);
|
else queueImageThumbnail(full, safeRel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const info = getInfo(safeRel) || {};
|
||||||
|
const rootFolder = rootFromRelPath(safeRel);
|
||||||
|
const added =
|
||||||
|
info.added ?? info.createdAt ?? null;
|
||||||
|
const completedAt = info.completedAt ?? null;
|
||||||
|
const tracker = info.tracker ?? null;
|
||||||
|
const torrentName = info.name ?? null;
|
||||||
|
const infoHash = info.infoHash ?? null;
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
name: safeRel,
|
name: safeRel,
|
||||||
size,
|
size,
|
||||||
type,
|
type,
|
||||||
url,
|
url,
|
||||||
thumbnail: thumb
|
thumbnail: thumb,
|
||||||
|
rootFolder,
|
||||||
|
added,
|
||||||
|
completedAt,
|
||||||
|
tracker,
|
||||||
|
torrentName,
|
||||||
|
infoHash
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user