Add DRM downloads, scrapers, gallery index, and UI improvements
- DRM video download pipeline with pywidevine subprocess for Widevine key acquisition - Scraper system: forum threads, Coomer/Kemono API, and MediaLink (Fapello) scrapers - SQLite-backed media index for instant gallery loads with startup scan - Duplicate detection and gallery filtering/sorting - HLS video component, log viewer, and scrape management UI - Dockerfile updated for Python/pywidevine, docker-compose volume for CDM Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
117
server/db.js
117
server/db.js
@@ -43,6 +43,23 @@ db.exec(`
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
folder TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
modified REAL NOT NULL,
|
||||
posted_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(folder, filename)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_folder ON media_files(folder);
|
||||
CREATE INDEX IF NOT EXISTS idx_media_type ON media_files(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_media_modified ON media_files(modified);
|
||||
CREATE INDEX IF NOT EXISTS idx_media_posted_at ON media_files(posted_at);
|
||||
`);
|
||||
|
||||
// Migration: add posted_at column if missing
|
||||
@@ -127,3 +144,103 @@ export function getDownloadStats() {
|
||||
'SELECT user_id, COUNT(*) as file_count, MAX(downloaded_at) as last_download FROM download_history GROUP BY user_id'
|
||||
).all();
|
||||
}
|
||||
|
||||
// --- media_files helpers ---
|
||||
|
||||
const upsertMediaStmt = db.prepare(`
|
||||
INSERT INTO media_files (folder, filename, type, size, modified, posted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(folder, filename) DO UPDATE SET
|
||||
size = excluded.size,
|
||||
modified = excluded.modified,
|
||||
posted_at = COALESCE(excluded.posted_at, media_files.posted_at)
|
||||
`);
|
||||
|
||||
export function upsertMediaFile(folder, filename, type, size, modified, postedAt) {
|
||||
upsertMediaStmt.run(folder, filename, type, size, modified, postedAt || null);
|
||||
}
|
||||
|
||||
export const upsertMediaFileBatch = db.transaction((files) => {
|
||||
for (const f of files) {
|
||||
upsertMediaStmt.run(f.folder, f.filename, f.type, f.size, f.modified, f.postedAt || null);
|
||||
}
|
||||
});
|
||||
|
||||
export function removeMediaFile(folder, filename) {
|
||||
db.prepare('DELETE FROM media_files WHERE folder = ? AND filename = ?').run(folder, filename);
|
||||
}
|
||||
|
||||
export function getMediaFolders() {
|
||||
return db.prepare(`
|
||||
SELECT folder AS name,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN type = 'image' THEN 1 ELSE 0 END) AS images,
|
||||
SUM(CASE WHEN type = 'video' THEN 1 ELSE 0 END) AS videos
|
||||
FROM media_files
|
||||
GROUP BY folder
|
||||
ORDER BY folder
|
||||
`).all();
|
||||
}
|
||||
|
||||
export function getMediaFiles({ folder, folders, type, sort, offset, limit }) {
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
|
||||
if (folder) {
|
||||
conditions.push('folder = ?');
|
||||
params.push(folder);
|
||||
} else if (folders && folders.length > 0) {
|
||||
conditions.push(`folder IN (${folders.map(() => '?').join(',')})`);
|
||||
params.push(...folders);
|
||||
}
|
||||
|
||||
if (type && type !== 'all') {
|
||||
conditions.push('type = ?');
|
||||
params.push(type);
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||
|
||||
const countRow = db.prepare(`SELECT COUNT(*) AS total FROM media_files ${where}`).get(...params);
|
||||
const total = countRow.total;
|
||||
|
||||
let orderBy;
|
||||
if (sort === 'shuffle') {
|
||||
orderBy = 'ORDER BY RANDOM()';
|
||||
} else {
|
||||
// 'latest' — prefer posted_at, fall back to modified
|
||||
orderBy = 'ORDER BY COALESCE(posted_at, datetime(modified / 1000, \'unixepoch\')) DESC';
|
||||
}
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT folder, filename, type, size, modified, posted_at
|
||||
FROM media_files
|
||||
${where}
|
||||
${orderBy}
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(...params, limit || 50, offset || 0);
|
||||
|
||||
return { total, rows };
|
||||
}
|
||||
|
||||
export function getAllIndexedFolders() {
|
||||
return db.prepare('SELECT DISTINCT folder FROM media_files').all().map(r => r.folder);
|
||||
}
|
||||
|
||||
export function removeStaleFiles(folder, existingFilenames) {
|
||||
const rows = db.prepare('SELECT filename FROM media_files WHERE folder = ?').all(folder);
|
||||
const existing = new Set(existingFilenames);
|
||||
const toDelete = rows.filter(r => !existing.has(r.filename));
|
||||
if (toDelete.length > 0) {
|
||||
const del = db.prepare('DELETE FROM media_files WHERE folder = ? AND filename = ?');
|
||||
const batch = db.transaction((files) => {
|
||||
for (const f of files) del.run(folder, f.filename);
|
||||
});
|
||||
batch(toDelete);
|
||||
}
|
||||
return toDelete.length;
|
||||
}
|
||||
|
||||
export function getMediaFileCount() {
|
||||
return db.prepare('SELECT COUNT(*) AS count FROM media_files').get().count;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user