fixed a bug & replicated all changes to docker version

This commit is contained in:
2025-12-27 19:23:22 +01:00
parent cc0b0a891e
commit bc74aa8116
25 changed files with 1876 additions and 509 deletions

View File

@@ -0,0 +1,43 @@
import {FastifyReply, FastifyRequest} from 'fastify';
import {getConfig, setConfig} from '../../shared/config';
export async function getFullConfig(req: FastifyRequest, reply: FastifyReply) {
try {
return getConfig();
} catch (err) {
return { error: "Error loading config" };
}
}
export async function getConfigSection(req: FastifyRequest<{ Params: { section: string } }>, reply: FastifyReply) {
try {
const { section } = req.params;
const config = getConfig();
if (config[section] === undefined) {
return { error: "Section not found" };
}
return { [section]: config[section] };
} catch (err) {
return { error: "Error loading config section" };
}
}
export async function updateConfig(req: FastifyRequest<{ Body: any }>, reply: FastifyReply) {
try {
return setConfig(req.body);
} catch (err) {
return { error: "Error updating config" };
}
}
export async function updateConfigSection(req: FastifyRequest<{ Params: { section: string }, Body: any }>, reply: FastifyReply) {
try {
const { section } = req.params;
const updatedConfig = setConfig({ [section]: req.body });
return { [section]: updatedConfig[section] };
} catch (err) {
return { error: "Error updating config section" };
}
}

View File

@@ -0,0 +1,11 @@
import { FastifyInstance } from 'fastify';
import * as controller from './config.controller';
async function configRoutes(fastify: FastifyInstance) {
fastify.get('/config', controller.getFullConfig);
fastify.get('/config/:section', controller.getConfigSection);
fastify.post('/config', controller.updateConfig);
fastify.post('/config/:section', controller.updateConfigSection);
}
export default configRoutes;

View File

@@ -0,0 +1,247 @@
import { FastifyRequest, FastifyReply } from 'fastify';
import { getConfig as loadConfig, setConfig as saveConfig } from '../../shared/config.js';
import { queryOne, queryAll, run } from '../../shared/database.js';
import crypto from 'crypto';
import fs from "fs";
import { PathLike } from "node:fs";
import path from "path";
import {getAnimeById, searchAnimeLocal} from "../anime/anime.service";
import {getBookById, searchBooksLocal} from "../books/books.service";
type SetConfigBody = {
library?: {
anime?: string | null;
manga?: string | null;
novels?: string | null;
};
};
type ScanQuery = {
mode?: 'full' | 'incremental';
};
type Params = {
type: 'anime' | 'manga' | 'novels';
id?: string;
};
async function resolveEntryMetadata(entry: any, type: string) {
let metadata = null;
let matchedId = entry.matched_id;
if (!matchedId) {
const query = entry.folder_name;
const results = type === 'anime'
? await searchAnimeLocal(query)
: await searchBooksLocal(query);
const first = results?.[0];
if (first?.id) {
matchedId = first.id;
await run(
`UPDATE local_entries
SET matched_id = ?, matched_source = 'anilist'
WHERE id = ?`,
[matchedId, entry.id],
'local_library'
);
}
}
if (matchedId) {
metadata = type === 'anime'
? await getAnimeById(matchedId)
: await getBookById(matchedId);
}
return {
id: entry.id,
type: entry.type,
matched: !!matchedId,
metadata
};
}
export async function scanLibrary(request: FastifyRequest<{ Querystring: ScanQuery }>, reply: FastifyReply) {
try {
const mode = request.query.mode || 'incremental';
const config = loadConfig();
if (!config.library) {
return reply.status(400).send({ error: 'NO_LIBRARY_CONFIGURED' });
}
if (mode === 'full') {
await run(`DELETE FROM local_files`, [], 'local_library');
await run(`DELETE FROM local_entries`, [], 'local_library');
}
for (const [type, basePath] of Object.entries(config.library)) {
if (!basePath || !fs.existsSync(<PathLike>basePath)) continue;
const dirs = fs.readdirSync(<string>basePath, { withFileTypes: true }).filter(d => d.isDirectory());
for (const dir of dirs) {
const fullPath = path.join(<string>basePath, dir.name);
const id = crypto.createHash('sha1').update(fullPath).digest('hex');
const now = Date.now();
const existing = await queryOne(`SELECT id FROM local_entries WHERE id = ?`, [id], 'local_library');
if (existing) {
await run(`UPDATE local_entries SET last_scan = ? WHERE id = ?`, [now, id], 'local_library');
await run(`DELETE FROM local_files WHERE entry_id = ?`, [id], 'local_library');
} else {
await run(
`INSERT INTO local_entries (id, type, path, folder_name, last_scan) VALUES (?, ?, ?, ?, ?)`,
[id, type, fullPath, dir.name, now],
'local_library'
);
}
const files = fs.readdirSync(fullPath, { withFileTypes: true })
.filter(f => f.isFile())
.sort((a, b) => a.name.localeCompare(b.name));
let unit = 1;
for (const file of files) {
await run(
`INSERT INTO local_files (id, entry_id, file_path, unit_number)
VALUES (?, ?, ?, ?)`,
[crypto.randomUUID(), id, path.join(fullPath, file.name), unit],
'local_library'
);
unit++;
}
}
}
return { status: 'OK' };
} catch (err) {
return reply.status(500).send({ error: 'FAILED_TO_SCAN_LIBRARY' });
}
}
export async function listEntries(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) {
try {
const { type } = request.params;
const entries = await queryAll(`SELECT * FROM local_entries WHERE type = ?`, [type], 'local_library');
return await Promise.all(entries.map((entry: any) => resolveEntryMetadata(entry, type)));
} catch {
return reply.status(500).send({ error: 'FAILED_TO_LIST_ENTRIES' });
}
}
export async function getEntry(request: FastifyRequest<{ Params: Params }>, reply: FastifyReply) {
try {
const { type, id } = request.params as { type: string, id: string };
const entry = await queryOne(
`SELECT * FROM local_entries WHERE matched_id = ? AND type = ?`,
[Number(id), type],
'local_library'
);
if (!entry) {
return reply.status(404).send({ error: 'ENTRY_NOT_FOUND' });
}
const [details, files] = await Promise.all([
resolveEntryMetadata(entry, type),
queryAll(
`SELECT id, file_path, unit_number FROM local_files WHERE entry_id = ? ORDER BY unit_number ASC`,
[id],
'local_library'
)
]);
return { ...details, files };
} catch {
return reply.status(500).send({ error: 'FAILED_TO_GET_ENTRY' });
}
}
export async function streamUnit(request: FastifyRequest, reply: FastifyReply) {
const { id, unit } = request.params as any;
const file = await queryOne(
`SELECT file_path FROM local_files WHERE entry_id = ? AND unit_number = ?`,
[id, unit],
'local_library'
);
if (!file || !fs.existsSync(file.file_path)) {
return reply.status(404).send({ error: 'FILE_NOT_FOUND' });
}
const stat = fs.statSync(file.file_path);
const range = request.headers.range;
if (!range) {
reply
.header('Content-Length', stat.size)
.header('Content-Type', 'video/mp4');
return fs.createReadStream(file.file_path);
}
const parts = range.replace(/bytes=/, '').split('-');
const start = Number(parts[0]);
let end = parts[1] ? Number(parts[1]) : stat.size - 1;
if (
Number.isNaN(start) ||
Number.isNaN(end) ||
start < 0 ||
end < start ||
end >= stat.size
) {
end = stat.size - 1;
}
reply
.status(206)
.header('Content-Range', `bytes ${start}-${end}/${stat.size}`)
.header('Accept-Ranges', 'bytes')
.header('Content-Length', end - start + 1)
.header('Content-Type', 'video/mp4');
return fs.createReadStream(file.file_path, { start, end });
}
type MatchBody = {
source: 'anilist';
matched_id: number | null;
};
export async function matchEntry(
request: FastifyRequest<{ Body: MatchBody }>,
reply: FastifyReply
) {
const { id, type } = request.params as any;
const { source, matched_id } = request.body;
const entry = await queryOne(
`SELECT id FROM local_entries WHERE id = ? AND type = ?`,
[id, type],
'local_library'
);
if (!entry) {
return reply.status(404).send({ error: 'ENTRY_NOT_FOUND' });
}
await run(
`UPDATE local_entries
SET matched_source = ?, matched_id = ?
WHERE id = ?`,
[source, matched_id, id],
'local_library'
);
return { status: 'OK', matched: !!matched_id };
}

View File

@@ -0,0 +1,12 @@
import { FastifyInstance } from 'fastify';
import * as controller from './local.controller';
async function localRoutes(fastify: FastifyInstance) {
fastify.post('/library/scan', controller.scanLibrary);
fastify.get('/library/:type', controller.listEntries);
fastify.get('/library/:type/:id', controller.getEntry);
fastify.get('/library/stream/:type/:id/:unit', controller.streamUnit);
fastify.post('/library/:type/:id/match', controller.matchEntry);
}
export default localRoutes;