| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- import {
- Body,
- Controller,
- Delete,
- Get,
- Param,
- Post,
- Put,
- Query,
- } from '@nestjs/common';
- import * as path from 'path';
- import { AppService } from './app.service';
- import { EventsGateway } from './events.gateway';
- interface FileRecord {
- dataset: string;
- input: string;
- output: string;
- date: string;
- }
- @Controller()
- export class AppController {
- constructor(
- private readonly appService: AppService,
- private readonly eventsGateway: EventsGateway,
- ) {}
- // List available datasets
- @Get('files')
- listDatasets() {
- return this.appService.listDatasets();
- }
- // List all datasets (including disabled ones)
- @Get('files/all-datasets')
- listAllDatasets() {
- return this.appService.listAllDatasets();
- }
- // Get total successful files across all datasets
- @Get('files/stats/successful')
- getTotalSuccessfulFiles() {
- return this.appService.getTotalSuccessfulFiles();
- }
- // Get total processed files across all datasets
- @Get('files/stats/processed')
- getTotalProcessedFiles() {
- return this.appService.getTotalProcessedFiles();
- }
- @Get()
- getRoot() {
- return {
- status: 'ok',
- message: 'Watch Finished API Service',
- datetime: new Date().toISOString(),
- uptime: process.uptime(),
- };
- }
- @Get('ready')
- getReady() {
- return { status: 'ready', datetime: new Date().toISOString() };
- }
- @Get('health')
- getHealth() {
- return { status: 'healthy', datetime: new Date().toISOString() };
- }
- // --- Unified files CRUD endpoints below ---
- // Create a file record
- @Post('files/:dataset/:file')
- createFile(
- @Param('dataset') dataset: string,
- @Param('file') file: string,
- @Body() payload: any,
- ) {
- const result = this.appService.setFile(dataset, file, payload);
- this.eventsGateway.emitFileUpdate({
- type: 'created',
- dataset,
- file,
- data: payload,
- });
- return result;
- }
- // Update a file record
- @Post('files/:dataset/:file/update')
- updateFile(
- @Param('dataset') dataset: string,
- @Param('file') file: string,
- @Body() payload: any,
- ) {
- const result = this.appService.setFile(dataset, file, payload);
- this.eventsGateway.emitFileUpdate({
- type: 'updated',
- dataset,
- file,
- data: payload,
- });
- return result;
- }
- // Read a file record
- @Get('files/:dataset/:file')
- readFile(@Param('dataset') dataset: string, @Param('file') file: string) {
- return this.appService.findFile(dataset, file);
- }
- // Destroy a file record (hard delete)
- @Delete('files/:dataset/:file')
- destroyFile(@Param('dataset') dataset: string, @Param('file') file: string) {
- const result = this.appService.removeFile(dataset, file, false);
- this.eventsGateway.emitFileUpdate({
- type: 'deleted',
- dataset,
- file,
- });
- return result;
- }
- // Clear all file records from database
- @Delete('files')
- clearAllFiles() {
- const result = this.appService.clearAllFiles();
- // Emit a general file update to notify clients
- this.eventsGateway.emitFileUpdate({
- type: 'cleared',
- dataset: '*',
- file: '*',
- });
- return { message: 'All file records cleared' };
- }
- // Requeue a file for processing (creates a task)
- @Post('files/:dataset/:file/requeue')
- requeueFile(
- @Param('dataset') dataset: string,
- @Param('file') file: string,
- @Body('preset') preset?: string,
- ) {
- // Get dataset configuration to find the preset and output configuration
- const datasetConfig = this.appService.getDatasetConfig();
- let processingPreset = preset;
- let output: string;
- // Try to find existing file record first
- const fileRecord = this.appService.findFile(dataset, file) as FileRecord;
- // Determine preset from dataset config (same logic as watcher)
- if (!processingPreset) {
- processingPreset = 'Fast 1080p30'; // Default fallback
- }
- if (datasetConfig[dataset]) {
- const datasetObj = datasetConfig[dataset];
- // Find the path configuration that matches this file
- for (const pathKey of Object.keys(datasetObj)) {
- if (pathKey !== 'enabled' && file.startsWith(pathKey)) {
- const pathConfig = datasetObj[pathKey];
- if (pathConfig && pathConfig.preset) {
- processingPreset = pathConfig.preset;
- break;
- }
- }
- }
- // Fallback to dataset-level preset if no path-specific preset found
- if (processingPreset === 'Fast 1080p30' && datasetObj.preset) {
- processingPreset = datasetObj.preset;
- }
- }
- // Determine destination and ext from dataset config
- let destination: string | undefined;
- let ext = '.mkv'; // Default extension
- if (datasetConfig[dataset]) {
- const datasetObj = datasetConfig[dataset];
- // Find the path configuration that matches this file
- for (const pathKey of Object.keys(datasetObj)) {
- if (pathKey !== 'enabled' && file.startsWith(pathKey)) {
- const pathConfig = datasetObj[pathKey];
- if (pathConfig) {
- if (pathConfig.destination) {
- destination = pathConfig.destination;
- }
- if (pathConfig.ext) {
- ext = pathConfig.ext.startsWith('.')
- ? pathConfig.ext
- : '.' + pathConfig.ext;
- }
- }
- break;
- }
- }
- // Fallback to dataset-level config
- if (!destination && datasetObj.destination) {
- destination = datasetObj.destination;
- }
- if (ext === '.mkv' && datasetObj.ext) {
- ext = datasetObj.ext.startsWith('.')
- ? datasetObj.ext
- : '.' + datasetObj.ext;
- }
- }
- if (fileRecord) {
- // Use existing file record
- output = fileRecord.output;
- } else {
- // No existing file record - determine output path from dataset config
- if (destination) {
- const fileName = path.basename(file, path.extname(file));
- output = path.join(destination, fileName + ext);
- } else {
- // Default: same directory with new extension
- output = path.join(
- path.dirname(file),
- path.basename(file, path.extname(file)) + ext,
- );
- }
- }
- // Fallback to default preset if no preset found
- if (!processingPreset) {
- processingPreset = 'Fast 1080p30';
- }
- // Create a task for processing
- const task = this.appService.createTask({
- dataset,
- input: file,
- output: output,
- preset: processingPreset,
- priority: 1, // Higher priority for manual requeues
- status: 'pending', // Mark as pending for processing
- });
- // Check if file already exists
- const existingFile = this.appService.findFile(dataset, file);
- // Update file record (create if doesn't exist, or update output if requeueing)
- this.appService.setFile(dataset, file, {
- date: new Date().toISOString(),
- output: output,
- });
- // Emit file update
- this.eventsGateway.emitFileUpdate({
- type: 'requeued',
- dataset,
- file,
- taskId: task.id,
- });
- // Emit task update to indicate task was created
- this.eventsGateway.emitTaskUpdate({
- type: 'created',
- taskId: task.id,
- task: 'handbrake',
- input: file,
- output: output,
- preset: processingPreset,
- });
- return { taskId: task.id, message: 'Task created for processing' };
- }
- @Get('files/:dataset')
- getAllFiles(@Param('dataset') dataset: string) {
- return this.appService.getAllFiles(dataset);
- }
- @Get('files/:dataset/deleted-older-than/:isoDate')
- getDeletedOlderThan(
- @Param('dataset') dataset: string,
- @Param('isoDate') isoDate: string,
- ) {
- return this.appService.getDeletedOlderThan(dataset, isoDate);
- }
- // --- Additional endpoints below ---
- @Post('handbrake/process')
- processWithHandbrake(
- @Body('input') input: string,
- @Body('output') output: string,
- @Body('preset') preset: string,
- ) {
- this.eventsGateway.emitTaskUpdate({
- type: 'started',
- task: 'handbrake',
- input,
- output,
- preset,
- });
- return this.appService.processWithHandbrake(input, output, preset);
- }
- @Get('handbrake/presets')
- getHandbrakePresets() {
- return this.appService.getHandbrakePresets();
- }
- @Post('maintenance/cleanup')
- cleanup(@Body('file') file: string, @Body('dirs') dirs: string[]) {
- const result = this.appService.cleanup(file, dirs);
- this.eventsGateway.emitMaintenanceUpdate({
- type: 'cleanup',
- file,
- dirs,
- });
- return result;
- }
- @Post('maintenance/purge')
- purge(
- @Body('dirs') dirs: string[],
- @Body('dayMs') dayMs?: number,
- @Body('cleanerMs') cleanerMs?: number,
- ) {
- const result = this.appService.purge(dirs, dayMs, cleanerMs);
- this.eventsGateway.emitMaintenanceUpdate({
- type: 'purge',
- dirs,
- dayMs,
- cleanerMs,
- });
- return result;
- }
- @Post('maintenance/prune')
- prune(@Body('dirs') dirs: string[]) {
- const result = this.appService.prune(dirs);
- this.eventsGateway.emitTaskUpdate({
- type: 'prune',
- dirs,
- });
- return result;
- }
- // Task maintenance endpoints
- @Post('maintenance/tasks/cleanup')
- cleanupTasks(
- @Body('status') status?: string,
- @Body('olderThanDays') olderThanDays?: number,
- ) {
- let result;
- if (status) {
- result = this.appService.cleanupTasksByStatus(status, olderThanDays);
- } else if (olderThanDays) {
- result = this.appService.cleanupOldTasks(olderThanDays);
- } else {
- // Default: cleanup completed and failed tasks older than 7 days
- result = this.appService.cleanupTasksByStatus('completed', 7);
- this.appService.cleanupTasksByStatus('failed', 7);
- this.appService.cleanupTasksByStatus('skipped', 7);
- }
- this.eventsGateway.emitTaskUpdate({
- type: 'task_cleanup',
- status,
- olderThanDays,
- });
- return result;
- }
- @Post('maintenance/tasks/archive')
- archiveTasks(@Body('daysOld') daysOld: number = 30): { changes?: number } {
- const result = this.appService.archiveOldTasks(daysOld);
- this.eventsGateway.emitTaskUpdate({
- type: 'task_archive',
- daysOld,
- });
- return result;
- }
- @Post('maintenance/tasks/purge')
- purgeAllTasks() {
- const result = this.appService.purgeAllTasks();
- this.eventsGateway.emitTaskUpdate({
- type: 'task_purge',
- });
- return result;
- }
- @Get('maintenance/tasks/stats')
- getTaskStats() {
- return this.appService.getTaskStats();
- }
- @Post('maintenance/tasks/scheduled-cleanup')
- runScheduledTaskCleanup() {
- const result = this.appService.scheduledTaskCleanup();
- this.eventsGateway.emitTaskUpdate({
- type: 'scheduled_task_cleanup',
- });
- return result;
- }
- // Duplicate file review endpoints
- @Post('maintenance/duplicates/scan')
- async scanDuplicates(@Body('resetExisting') resetExisting?: boolean) {
- return await this.appService.scanDuplicateFiles(resetExisting);
- }
- @Get('maintenance/duplicates')
- listDuplicates(
- @Query('status') status?: string,
- @Query('dataset') dataset?: string,
- ) {
- return this.appService.listDuplicateGroups(status, dataset);
- }
- @Post('maintenance/duplicates/:id/mark')
- markDuplicate(
- @Param('id') id: string,
- @Body('status') status: 'pending' | 'reviewed' | 'purged',
- @Body('note') note?: string,
- ) {
- return this.appService.markDuplicateGroup(Number(id), status, note);
- }
- @Post('maintenance/duplicates/:id/purge')
- purgeDuplicate(
- @Param('id') id: string,
- @Body('files') files: string[],
- @Body('note') note?: string,
- ) {
- return this.appService.purgeDuplicateFiles(Number(id), files || [], note);
- }
- @Get('config/settings')
- getSettings(
- @Query('key') key?: string,
- @Query('default') defaultValue?: any,
- ) {
- return this.appService.getSettings(key, defaultValue);
- }
- @Get('config/settings/:key')
- getSetting(@Param('key') key: string) {
- return this.appService.getSettings(key);
- }
- @Post('config/settings')
- setSettings(@Body() settings: Record<string, any>) {
- // console.log('Received setSettings request:', settings);
- try {
- const result = this.appService.setSettings(settings);
- // console.log('setSettings result:', result);
- this.eventsGateway.emitSettingsUpdate({
- type: 'settings',
- action: 'update',
- settings,
- });
- return result;
- } catch (error) {
- console.error('Error in setSettings controller:', error);
- throw error;
- }
- }
- @Delete('config/settings/:key')
- deleteSetting(@Param('key') key: string) {
- const result = this.appService.deleteSetting(key);
- this.eventsGateway.emitSettingsUpdate({
- type: 'settings',
- action: 'delete',
- key,
- });
- return result;
- }
- @Get('config/file/:name')
- getConfigFile(@Param('name') name: string) {
- return this.appService.getConfigFile(name);
- }
- @Get('config/list')
- listConfigs() {
- return this.appService.listConfigs();
- }
- @Post('watcher/start')
- startWatcher(
- @Body('watches') watches: string[],
- @Body('options') options?: any,
- ) {
- return this.appService.startWatcher(watches, options);
- }
- @Post('watcher/stop')
- async stopWatcher() {
- return this.appService.stopWatcher();
- }
- @Get('watcher/status')
- watcherStatus() {
- return this.appService.watcherStatus();
- }
- @Post('files/expire')
- deleteExpiredFiles(@Body('days') days?: number) {
- return { deleted: this.appService.deleteExpiredFiles(days) };
- }
- @Post('files/migrate')
- migrateJsonToSqlite(@Body() opts: { datasets?: string[]; dataDir?: string }) {
- this.appService.migrateJsonToSqlite(opts);
- return { migrated: true };
- }
- // Task management endpoints
- @Get('tasks')
- getAllTasks() {
- return this.appService.getAllTasks();
- }
- @Get('tasks/queue/status')
- getQueueStatus() {
- return this.appService.getQueueStatus();
- }
- @Get('tasks/queue/settings')
- getQueueSettings() {
- return this.appService.getQueueSettings();
- }
- @Post('tasks/queue/settings')
- updateQueueSettings(@Body() settings: any) {
- return this.appService.updateQueueSettings(settings);
- }
- @Get('tasks/processing-status')
- getTaskProcessingStatus() {
- return this.appService.taskProcessingStatus();
- }
- @Get('tasks/:id')
- getTaskById(@Param('id') id: string) {
- return this.appService.getTaskById(parseInt(id));
- }
- @Delete('tasks/:id')
- deleteTask(@Param('id') id: string) {
- return this.appService.deleteTask(parseInt(id));
- }
- @Post('tasks/start-processing')
- startTaskProcessing() {
- return this.appService.startTaskProcessing();
- }
- @Post('tasks/stop-processing')
- stopTaskProcessing(@Body('graceful') graceful?: boolean) {
- const shouldGraceful = graceful !== false;
- return this.appService.stopTaskProcessing(shouldGraceful);
- }
- // Task management endpoints
- @Get('tasks')
- getTasks() {
- return this.appService.getAllTasks();
- }
- @Get('tasks/:id')
- getTask(@Param('id') id: string) {
- return this.appService.getTaskById(parseInt(id));
- }
- @Put('tasks/:id')
- updateTask(@Param('id') id: string, @Body() updates: any) {
- return this.appService.updateTask(parseInt(id), updates);
- }
- @Post('tasks')
- createTask(@Body() taskData: any) {
- return this.appService.createTask(taskData);
- }
- }
|