163 lines
6.3 KiB
TypeScript
163 lines
6.3 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* One-time import script to populate the database from the existing library
|
|
*
|
|
* Usage:
|
|
* pnpm import
|
|
* # or
|
|
* tsx scripts/import.ts
|
|
*/
|
|
|
|
import Database from 'better-sqlite3';
|
|
import { join, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { existsSync, mkdirSync } from 'fs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DATA_DIR = process.env.DATA_DIR || join(__dirname, '../../../data');
|
|
const DB_PATH = process.env.DATABASE_URL || join(DATA_DIR, 'library.db');
|
|
|
|
// Ensure data directory exists
|
|
if (!existsSync(DATA_DIR)) {
|
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
console.log('╔════════════════════════════════════════════╗');
|
|
console.log('║ MEDIA LIBRARY IMPORT SCRIPT ║');
|
|
console.log('╠════════════════════════════════════════════╣');
|
|
console.log('║ This will scan the library and populate ║');
|
|
console.log('║ the database with video metadata. ║');
|
|
console.log('╚════════════════════════════════════════════╝');
|
|
console.log('');
|
|
|
|
// Step 0: Initialize database schema
|
|
console.log('Step 0: Initializing database schema...');
|
|
console.log(` Database: ${DB_PATH}`);
|
|
|
|
const sqlite = new Database(DB_PATH);
|
|
sqlite.pragma('journal_mode = WAL');
|
|
|
|
sqlite.exec(`
|
|
CREATE TABLE IF NOT EXISTS videos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
path TEXT NOT NULL UNIQUE,
|
|
filename TEXT NOT NULL,
|
|
studio TEXT,
|
|
performer TEXT,
|
|
title TEXT,
|
|
duration_seconds INTEGER,
|
|
quality TEXT,
|
|
orientation TEXT CHECK(orientation IN ('H', 'V')),
|
|
has_audio INTEGER DEFAULT 1,
|
|
file_size INTEGER,
|
|
file_hash TEXT,
|
|
last_validated INTEGER,
|
|
is_valid INTEGER DEFAULT 1,
|
|
thumbnail_path TEXT,
|
|
created_at INTEGER DEFAULT (unixepoch()),
|
|
tags TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_orientation ON videos(orientation);
|
|
CREATE INDEX IF NOT EXISTS idx_studio ON videos(studio);
|
|
CREATE INDEX IF NOT EXISTS idx_is_valid ON videos(is_valid);
|
|
|
|
CREATE TABLE IF NOT EXISTS compilations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
filename TEXT NOT NULL,
|
|
path TEXT,
|
|
duration_seconds INTEGER,
|
|
video_ids TEXT,
|
|
settings TEXT,
|
|
created_at INTEGER DEFAULT (unixepoch())
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
type TEXT NOT NULL CHECK(type IN ('compilation', 'scan', 'organize')),
|
|
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'running', 'completed', 'failed')),
|
|
progress INTEGER DEFAULT 0,
|
|
log TEXT,
|
|
params TEXT,
|
|
started_at INTEGER,
|
|
completed_at INTEGER,
|
|
created_at INTEGER DEFAULT (unixepoch())
|
|
);
|
|
`);
|
|
|
|
sqlite.close();
|
|
console.log(' Schema initialized successfully!\n');
|
|
|
|
// Now import the scanner (which will use the existing db)
|
|
const { scanLibrary, scanCompilations } = await import('../src/services/scanner.js');
|
|
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Step 1: Scan compilations from playback directory
|
|
console.log('Step 1: Scanning compilations...\n');
|
|
const compilationResult = await scanCompilations((progress) => {
|
|
process.stdout.write(`\r [${progress.processed}/${progress.total}] ${progress.current.slice(0, 50).padEnd(50)}`);
|
|
});
|
|
console.log('\n');
|
|
console.log(` Compilations imported: ${compilationResult.imported}`);
|
|
console.log(` Compilations skipped: ${compilationResult.skipped}`);
|
|
console.log(` Compilations errors: ${compilationResult.errors}`);
|
|
console.log(` Compilations removed: ${compilationResult.removed}`);
|
|
console.log('');
|
|
|
|
// Step 2: Scan full video library for compilation source material
|
|
console.log('Step 2: Scanning full video library for compilation source...\n');
|
|
const result = await scanLibrary(
|
|
(progress) => {
|
|
const percent = Math.floor((progress.processed / progress.total) * 100);
|
|
const bar = '█'.repeat(Math.floor(percent / 2.5)) + '░'.repeat(40 - Math.floor(percent / 2.5));
|
|
process.stdout.write(`\r[${bar}] ${percent}% (${progress.processed}/${progress.total}) ${progress.current.slice(0, 40).padEnd(40)}`);
|
|
},
|
|
{
|
|
skipExisting: true,
|
|
validateFiles: false, // Skip validation for speed
|
|
scanSource: 'library', // Scan full library for compilation source
|
|
}
|
|
);
|
|
|
|
// Step 3: Also scan public directory for public gallery display
|
|
console.log('\n\nStep 3: Scanning public directory for gallery display...\n');
|
|
const publicResult = await scanLibrary(
|
|
(progress) => {
|
|
const percent = Math.floor((progress.processed / progress.total) * 100);
|
|
const bar = '█'.repeat(Math.floor(percent / 2.5)) + '░'.repeat(40 - Math.floor(percent / 2.5));
|
|
process.stdout.write(`\r[${bar}] ${percent}% (${progress.processed}/${progress.total}) ${progress.current.slice(0, 40).padEnd(40)}`);
|
|
},
|
|
{
|
|
skipExisting: true,
|
|
validateFiles: false,
|
|
scanSource: 'public', // Scan public directory
|
|
}
|
|
);
|
|
|
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
|
|
console.log('\n');
|
|
console.log('════════════════════════════════════════════');
|
|
console.log(' COMPLETE ');
|
|
console.log('════════════════════════════════════════════');
|
|
console.log(' Library (compilation source):');
|
|
console.log(` Imported: ${result.imported}`);
|
|
console.log(` Skipped: ${result.skipped}`);
|
|
console.log(` Errors: ${result.errors}`);
|
|
console.log(` Removed: ${result.removed}`);
|
|
console.log('');
|
|
console.log(' Public (gallery display):');
|
|
console.log(` Imported: ${publicResult.imported}`);
|
|
console.log(` Skipped: ${publicResult.skipped}`);
|
|
console.log(` Errors: ${publicResult.errors}`);
|
|
console.log(` Removed: ${publicResult.removed}`);
|
|
console.log('');
|
|
console.log(` Time: ${elapsed}s`);
|
|
console.log('════════════════════════════════════════════');
|
|
} catch (err) {
|
|
console.error('\nImport failed:', err);
|
|
process.exit(1);
|
|
}
|