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:
2025-10-26 22:19:10 +03:00
parent be4ffd6575
commit acc38d419c
4 changed files with 1023 additions and 140 deletions

View File

@@ -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_EXTS = [".mp4", ".webm", ".mkv", ".mov", ".m4v"];
const generatingThumbnails = new Set();
const INFO_FILENAME = "info.json";
app.use(cors());
app.use(express.json());
@@ -59,6 +60,70 @@ function ensureDirForFile(filePath) {
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) {
return relPath.replace(/^[\\/]+/, "");
}
@@ -181,14 +246,28 @@ function cleanupEmptyDirs(startDir) {
while (
dir &&
dir.startsWith(THUMBNAIL_DIR) &&
fs.existsSync(dir) &&
fs.lstatSync(dir).isDirectory()
fs.existsSync(dir)
) {
const entries = fs.readdirSync(dir);
if (entries.length > 0) break;
fs.rmdirSync(dir);
dir = path.dirname(dir);
if (dir === THUMBNAIL_DIR || dir === path.dirname(THUMBNAIL_DIR)) break;
try {
const stat = fs.lstatSync(dir);
if (!stat.isDirectory()) break;
const entries = fs.readdirSync(dir);
if (entries.length > 0) break;
fs.rmdirSync(dir);
} catch (err) {
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,
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({
ok: true,
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);
}
upsertInfoFile(entry.savePath, {
completedAt: Date.now(),
totalBytes: torrent.downloaded,
fileCount: torrent.files.length
});
broadcastFileUpdate(rootFolder);
broadcastSnapshot();
});
} 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ş ---
const walk = (dir) => {
let result = [];
@@ -561,6 +667,8 @@ app.get("/api/files", requireAuth, (req, res) => {
if (entry.isDirectory()) {
result = result.concat(walk(full));
} else {
if (entry.name.toLowerCase() === INFO_FILENAME) continue;
const size = fs.statSync(full).size;
const type = mime.lookup(full) || "application/octet-stream";
@@ -588,12 +696,27 @@ app.get("/api/files", requireAuth, (req, res) => {
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({
name: safeRel,
size,
type,
url,
thumbnail: thumb
thumbnail: thumb,
rootFolder,
added,
completedAt,
tracker,
torrentName,
infoHash
});
}
}