feat(torrent): library.js SQLite + purge selection (ref #917)

Adds SQLite-backed torrent library (add/list/get/touch/keep/remove) and
the expiredEphemeral() query the purge job uses to find stale
kept=0 torrents (last_played_at < now-ttl). Includes package-lock.json
and a lxc/app .gitignore for node_modules.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-28 15:00:32 +02:00
parent d45803db0c
commit 7aaa8d5409
4 changed files with 3134 additions and 0 deletions

View File

@ -0,0 +1 @@
node_modules/

View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
// SecuBox-Deb :: secubox-torrent :: library.js — CyberMind https://cybermind.fr
//
// SQLite-backed torrent library: ephemeral (kept=0) vs. kept torrents, plus
// the selection query used by the purge job to find stale ephemeral entries.
import Database from 'better-sqlite3';
export class Library {
constructor(dbPath) {
this.db = new Database(dbPath);
this.db.exec(`CREATE TABLE IF NOT EXISTS torrents (
infohash TEXT PRIMARY KEY, name TEXT, magnet TEXT, path TEXT,
added_at INTEGER, last_played_at INTEGER, kept INTEGER DEFAULT 0)`);
}
add({ infohash, name, magnet, path }) {
const now = Math.floor(Date.now() / 1000);
this.db.prepare(`INSERT OR REPLACE INTO torrents
(infohash,name,magnet,path,added_at,last_played_at,kept)
VALUES (?,?,?,?,?,?,0)`).run(infohash, name, magnet, path, now, now);
}
list() { return this.db.prepare('SELECT * FROM torrents ORDER BY added_at DESC').all(); }
get(infohash) { return this.db.prepare('SELECT * FROM torrents WHERE infohash=?').get(infohash) || null; }
touch(infohash) { this.touchAt(infohash, Math.floor(Date.now() / 1000)); }
touchAt(infohash, ts) { this.db.prepare('UPDATE torrents SET last_played_at=? WHERE infohash=?').run(ts, infohash); }
keep(infohash, newPath) { this.db.prepare('UPDATE torrents SET kept=1, path=? WHERE infohash=?').run(newPath, infohash); }
remove(infohash) { this.db.prepare('DELETE FROM torrents WHERE infohash=?').run(infohash); }
expiredEphemeral(ttlSeconds, now = Math.floor(Date.now() / 1000)) {
const cutoff = now - ttlSeconds;
return this.db.prepare('SELECT infohash FROM torrents WHERE kept=0 AND last_played_at < ?')
.all(cutoff).map(r => r.infohash);
}
}

View File

@ -0,0 +1,38 @@
// SPDX-License-Identifier: LicenseRef-CMSD-1.0
// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
// SecuBox-Deb :: secubox-torrent :: library.test.js — CyberMind https://cybermind.fr
//
// Tests for the SQLite-backed torrent library + ephemeral purge selection.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { Library } from './library.js';
const mk = () => new Library(':memory:');
test('add then list yields one ephemeral row', () => {
const lib = mk();
lib.add({ infohash: 'a', name: 'A', magnet: 'm', path: '/tmp/a' });
const rows = lib.list();
assert.equal(rows.length, 1);
assert.equal(rows[0].kept, 0);
});
test('keep flips kept and updates path', () => {
const lib = mk();
lib.add({ infohash: 'a', name: 'A', magnet: 'm', path: '/tmp/a' });
lib.keep('a', '/data/torrent/library/a');
assert.equal(lib.get('a').kept, 1);
assert.equal(lib.get('a').path, '/data/torrent/library/a');
});
test('expiredEphemeral returns only stale unkept torrents', () => {
const lib = mk();
lib.add({ infohash: 'old', name: 'O', magnet: 'm', path: '/tmp/o' });
lib.add({ infohash: 'fresh', name: 'F', magnet: 'm', path: '/tmp/f' });
lib.add({ infohash: 'kept', name: 'K', magnet: 'm', path: '/tmp/k' });
lib.touchAt('old', 1000); lib.touchAt('fresh', 9000); lib.touchAt('kept', 1000);
lib.keep('kept', '/data/k');
const exp = lib.expiredEphemeral(3600, 10000); // now=10000, ttl=3600 → cutoff 6400
assert.deepEqual(exp, ['old']);
});

File diff suppressed because it is too large Load Diff