app.controller.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. import {
  2. Body,
  3. Controller,
  4. Delete,
  5. Get,
  6. Param,
  7. Post,
  8. Put,
  9. Query,
  10. } from '@nestjs/common';
  11. import * as path from 'path';
  12. import { AppService } from './app.service';
  13. import { EventsGateway } from './events.gateway';
  14. interface FileRecord {
  15. dataset: string;
  16. input: string;
  17. output: string;
  18. date: string;
  19. }
  20. @Controller()
  21. export class AppController {
  22. constructor(
  23. private readonly appService: AppService,
  24. private readonly eventsGateway: EventsGateway,
  25. ) {}
  26. // List available datasets
  27. @Get('files')
  28. listDatasets() {
  29. return this.appService.listDatasets();
  30. }
  31. // List all datasets (including disabled ones)
  32. @Get('files/all-datasets')
  33. listAllDatasets() {
  34. return this.appService.listAllDatasets();
  35. }
  36. // Get total successful files across all datasets
  37. @Get('files/stats/successful')
  38. getTotalSuccessfulFiles() {
  39. return this.appService.getTotalSuccessfulFiles();
  40. }
  41. // Get total processed files across all datasets
  42. @Get('files/stats/processed')
  43. getTotalProcessedFiles() {
  44. return this.appService.getTotalProcessedFiles();
  45. }
  46. @Get()
  47. getRoot() {
  48. return {
  49. status: 'ok',
  50. message: 'Watch Finished API Service',
  51. datetime: new Date().toISOString(),
  52. uptime: process.uptime(),
  53. };
  54. }
  55. @Get('ready')
  56. getReady() {
  57. return { status: 'ready', datetime: new Date().toISOString() };
  58. }
  59. @Get('health')
  60. getHealth() {
  61. return { status: 'healthy', datetime: new Date().toISOString() };
  62. }
  63. // --- Unified files CRUD endpoints below ---
  64. // Create a file record
  65. @Post('files/:dataset/:file')
  66. createFile(
  67. @Param('dataset') dataset: string,
  68. @Param('file') file: string,
  69. @Body() payload: any,
  70. ) {
  71. const result = this.appService.setFile(dataset, file, payload);
  72. this.eventsGateway.emitFileUpdate({
  73. type: 'created',
  74. dataset,
  75. file,
  76. data: payload,
  77. });
  78. return result;
  79. }
  80. // Update a file record
  81. @Post('files/:dataset/:file/update')
  82. updateFile(
  83. @Param('dataset') dataset: string,
  84. @Param('file') file: string,
  85. @Body() payload: any,
  86. ) {
  87. const result = this.appService.setFile(dataset, file, payload);
  88. this.eventsGateway.emitFileUpdate({
  89. type: 'updated',
  90. dataset,
  91. file,
  92. data: payload,
  93. });
  94. return result;
  95. }
  96. // Read a file record
  97. @Get('files/:dataset/:file')
  98. readFile(@Param('dataset') dataset: string, @Param('file') file: string) {
  99. return this.appService.findFile(dataset, file);
  100. }
  101. // Destroy a file record (hard delete)
  102. @Delete('files/:dataset/:file')
  103. destroyFile(@Param('dataset') dataset: string, @Param('file') file: string) {
  104. const result = this.appService.removeFile(dataset, file, false);
  105. this.eventsGateway.emitFileUpdate({
  106. type: 'deleted',
  107. dataset,
  108. file,
  109. });
  110. return result;
  111. }
  112. // Clear all file records from database
  113. @Delete('files')
  114. clearAllFiles() {
  115. const result = this.appService.clearAllFiles();
  116. // Emit a general file update to notify clients
  117. this.eventsGateway.emitFileUpdate({
  118. type: 'cleared',
  119. dataset: '*',
  120. file: '*',
  121. });
  122. return { message: 'All file records cleared' };
  123. }
  124. // Requeue a file for processing (creates a task)
  125. @Post('files/:dataset/:file/requeue')
  126. requeueFile(
  127. @Param('dataset') dataset: string,
  128. @Param('file') file: string,
  129. @Body('preset') preset?: string,
  130. ) {
  131. // Get dataset configuration to find the preset and output configuration
  132. const datasetConfig = this.appService.getDatasetConfig();
  133. let processingPreset = preset;
  134. let output: string;
  135. // Try to find existing file record first
  136. const fileRecord = this.appService.findFile(dataset, file) as FileRecord;
  137. // Determine preset from dataset config (same logic as watcher)
  138. if (!processingPreset) {
  139. processingPreset = 'Fast 1080p30'; // Default fallback
  140. }
  141. if (datasetConfig[dataset]) {
  142. const datasetObj = datasetConfig[dataset];
  143. // Find the path configuration that matches this file
  144. for (const pathKey of Object.keys(datasetObj)) {
  145. if (pathKey !== 'enabled' && file.startsWith(pathKey)) {
  146. const pathConfig = datasetObj[pathKey];
  147. if (pathConfig && pathConfig.preset) {
  148. processingPreset = pathConfig.preset;
  149. break;
  150. }
  151. }
  152. }
  153. // Fallback to dataset-level preset if no path-specific preset found
  154. if (processingPreset === 'Fast 1080p30' && datasetObj.preset) {
  155. processingPreset = datasetObj.preset;
  156. }
  157. }
  158. console.log(`Final processingPreset: ${processingPreset}`);
  159. // Determine destination and ext from dataset config
  160. let destination: string | undefined;
  161. let ext = '.mkv'; // Default extension
  162. if (datasetConfig[dataset]) {
  163. const datasetObj = datasetConfig[dataset];
  164. // Find the path configuration that matches this file
  165. for (const pathKey of Object.keys(datasetObj)) {
  166. if (pathKey !== 'enabled' && file.startsWith(pathKey)) {
  167. const pathConfig = datasetObj[pathKey];
  168. if (pathConfig) {
  169. if (pathConfig.destination) {
  170. destination = pathConfig.destination;
  171. }
  172. if (pathConfig.ext) {
  173. ext = pathConfig.ext.startsWith('.')
  174. ? pathConfig.ext
  175. : '.' + pathConfig.ext;
  176. }
  177. }
  178. break;
  179. }
  180. }
  181. // Fallback to dataset-level config
  182. if (!destination && datasetObj.destination) {
  183. destination = datasetObj.destination;
  184. }
  185. if (ext === '.mkv' && datasetObj.ext) {
  186. ext = datasetObj.ext.startsWith('.')
  187. ? datasetObj.ext
  188. : '.' + datasetObj.ext;
  189. }
  190. }
  191. if (fileRecord) {
  192. // Use existing file record
  193. output = fileRecord.output;
  194. } else {
  195. // No existing file record - determine output path from dataset config
  196. if (destination) {
  197. const fileName = path.basename(file, path.extname(file));
  198. output = path.join(destination, fileName + ext);
  199. } else {
  200. // Default: same directory with new extension
  201. output = path.join(
  202. path.dirname(file),
  203. path.basename(file, path.extname(file)) + ext,
  204. );
  205. }
  206. }
  207. // Fallback to default preset if no preset found
  208. if (!processingPreset) {
  209. processingPreset = 'Fast 1080p30';
  210. }
  211. // Create a task for processing
  212. const task = this.appService.createTask({
  213. dataset,
  214. input: file,
  215. output: output,
  216. preset: processingPreset,
  217. priority: 1, // Higher priority for manual requeues
  218. status: 'pending', // Mark as pending for processing
  219. });
  220. // Check if file already exists
  221. const existingFile = this.appService.findFile(dataset, file);
  222. // Update file record (create if doesn't exist, or update output if requeueing)
  223. this.appService.setFile(dataset, file, {
  224. date: new Date().toISOString(),
  225. output: output,
  226. });
  227. // Emit file update
  228. this.eventsGateway.emitFileUpdate({
  229. type: 'requeued',
  230. dataset,
  231. file,
  232. taskId: task.id,
  233. });
  234. // Emit task update to indicate task was created
  235. this.eventsGateway.emitTaskUpdate({
  236. type: 'created',
  237. taskId: task.id,
  238. task: 'handbrake',
  239. input: file,
  240. output: output,
  241. preset: processingPreset,
  242. });
  243. return { taskId: task.id, message: 'Task created for processing' };
  244. }
  245. @Get('files/:dataset')
  246. getAllFiles(@Param('dataset') dataset: string) {
  247. return this.appService.getAllFiles(dataset);
  248. }
  249. @Get('files/:dataset/deleted-older-than/:isoDate')
  250. getDeletedOlderThan(
  251. @Param('dataset') dataset: string,
  252. @Param('isoDate') isoDate: string,
  253. ) {
  254. return this.appService.getDeletedOlderThan(dataset, isoDate);
  255. }
  256. // --- Additional endpoints below ---
  257. @Post('handbrake/process')
  258. processWithHandbrake(
  259. @Body('input') input: string,
  260. @Body('output') output: string,
  261. @Body('preset') preset: string,
  262. ) {
  263. this.eventsGateway.emitTaskUpdate({
  264. type: 'started',
  265. task: 'handbrake',
  266. input,
  267. output,
  268. preset,
  269. });
  270. return this.appService.processWithHandbrake(input, output, preset);
  271. }
  272. @Get('handbrake/presets')
  273. getHandbrakePresets() {
  274. return this.appService.getHandbrakePresets();
  275. }
  276. @Post('maintenance/cleanup')
  277. cleanup(@Body('file') file: string, @Body('dirs') dirs: string[]) {
  278. const result = this.appService.cleanup(file, dirs);
  279. this.eventsGateway.emitMaintenanceUpdate({
  280. type: 'cleanup',
  281. file,
  282. dirs,
  283. });
  284. return result;
  285. }
  286. @Post('maintenance/purge')
  287. purge(
  288. @Body('dirs') dirs: string[],
  289. @Body('dayMs') dayMs?: number,
  290. @Body('cleanerMs') cleanerMs?: number,
  291. ) {
  292. const result = this.appService.purge(dirs, dayMs, cleanerMs);
  293. this.eventsGateway.emitMaintenanceUpdate({
  294. type: 'purge',
  295. dirs,
  296. dayMs,
  297. cleanerMs,
  298. });
  299. return result;
  300. }
  301. @Post('maintenance/prune')
  302. prune(@Body('dirs') dirs: string[]) {
  303. const result = this.appService.prune(dirs);
  304. this.eventsGateway.emitTaskUpdate({
  305. type: 'prune',
  306. dirs,
  307. });
  308. return result;
  309. }
  310. // Task maintenance endpoints
  311. @Post('maintenance/tasks/cleanup')
  312. cleanupTasks(
  313. @Body('status') status?: string,
  314. @Body('olderThanDays') olderThanDays?: number,
  315. ) {
  316. let result;
  317. if (status) {
  318. result = this.appService.cleanupTasksByStatus(status, olderThanDays);
  319. } else if (olderThanDays) {
  320. result = this.appService.cleanupOldTasks(olderThanDays);
  321. } else {
  322. // Default: cleanup completed and failed tasks older than 7 days
  323. result = this.appService.cleanupTasksByStatus('completed', 7);
  324. this.appService.cleanupTasksByStatus('failed', 7);
  325. this.appService.cleanupTasksByStatus('skipped', 7);
  326. }
  327. this.eventsGateway.emitTaskUpdate({
  328. type: 'task_cleanup',
  329. status,
  330. olderThanDays,
  331. });
  332. return result;
  333. }
  334. @Post('maintenance/tasks/archive')
  335. archiveTasks(@Body('daysOld') daysOld: number = 30): { changes?: number } {
  336. const result = this.appService.archiveOldTasks(daysOld);
  337. this.eventsGateway.emitTaskUpdate({
  338. type: 'task_archive',
  339. daysOld,
  340. });
  341. return result;
  342. }
  343. @Post('maintenance/tasks/purge')
  344. purgeAllTasks() {
  345. const result = this.appService.purgeAllTasks();
  346. this.eventsGateway.emitTaskUpdate({
  347. type: 'task_purge',
  348. });
  349. return result;
  350. }
  351. @Get('maintenance/tasks/stats')
  352. getTaskStats() {
  353. return this.appService.getTaskStats();
  354. }
  355. @Post('maintenance/tasks/scheduled-cleanup')
  356. runScheduledTaskCleanup() {
  357. const result = this.appService.scheduledTaskCleanup();
  358. this.eventsGateway.emitTaskUpdate({
  359. type: 'scheduled_task_cleanup',
  360. });
  361. return result;
  362. }
  363. @Get('config/settings')
  364. getSettings(
  365. @Query('key') key?: string,
  366. @Query('default') defaultValue?: any,
  367. ) {
  368. return this.appService.getSettings(key, defaultValue);
  369. }
  370. @Get('config/settings/:key')
  371. getSetting(@Param('key') key: string) {
  372. return this.appService.getSettings(key);
  373. }
  374. @Post('config/settings')
  375. setSettings(@Body() settings: Record<string, any>) {
  376. // console.log('Received setSettings request:', settings);
  377. try {
  378. const result = this.appService.setSettings(settings);
  379. // console.log('setSettings result:', result);
  380. this.eventsGateway.emitSettingsUpdate({
  381. type: 'settings',
  382. action: 'update',
  383. settings,
  384. });
  385. return result;
  386. } catch (error) {
  387. console.error('Error in setSettings controller:', error);
  388. throw error;
  389. }
  390. }
  391. @Delete('config/settings/:key')
  392. deleteSetting(@Param('key') key: string) {
  393. const result = this.appService.deleteSetting(key);
  394. this.eventsGateway.emitSettingsUpdate({
  395. type: 'settings',
  396. action: 'delete',
  397. key,
  398. });
  399. return result;
  400. }
  401. @Get('config/file/:name')
  402. getConfigFile(@Param('name') name: string) {
  403. return this.appService.getConfigFile(name);
  404. }
  405. @Get('config/list')
  406. listConfigs() {
  407. return this.appService.listConfigs();
  408. }
  409. @Post('watcher/start')
  410. startWatcher(
  411. @Body('watches') watches: string[],
  412. @Body('options') options?: any,
  413. ) {
  414. return this.appService.startWatcher(watches, options);
  415. }
  416. @Post('watcher/stop')
  417. async stopWatcher() {
  418. return this.appService.stopWatcher();
  419. }
  420. @Get('watcher/status')
  421. watcherStatus() {
  422. return this.appService.watcherStatus();
  423. }
  424. @Post('files/expire')
  425. deleteExpiredFiles(@Body('days') days?: number) {
  426. return { deleted: this.appService.deleteExpiredFiles(days) };
  427. }
  428. @Post('files/migrate')
  429. migrateJsonToSqlite(@Body() opts: { datasets?: string[]; dataDir?: string }) {
  430. this.appService.migrateJsonToSqlite(opts);
  431. return { migrated: true };
  432. }
  433. // Task management endpoints
  434. @Get('tasks')
  435. getAllTasks() {
  436. return this.appService.getAllTasks();
  437. }
  438. @Get('tasks/queue/status')
  439. getQueueStatus() {
  440. return this.appService.getQueueStatus();
  441. }
  442. @Get('tasks/queue/settings')
  443. getQueueSettings() {
  444. return this.appService.getQueueSettings();
  445. }
  446. @Post('tasks/queue/settings')
  447. updateQueueSettings(@Body() settings: any) {
  448. return this.appService.updateQueueSettings(settings);
  449. }
  450. @Get('tasks/processing-status')
  451. getTaskProcessingStatus() {
  452. return this.appService.taskProcessingStatus();
  453. }
  454. @Get('tasks/:id')
  455. getTaskById(@Param('id') id: string) {
  456. return this.appService.getTaskById(parseInt(id));
  457. }
  458. @Delete('tasks/:id')
  459. deleteTask(@Param('id') id: string) {
  460. return this.appService.deleteTask(parseInt(id));
  461. }
  462. @Post('tasks/start-processing')
  463. startTaskProcessing() {
  464. return this.appService.startTaskProcessing();
  465. }
  466. @Post('tasks/stop-processing')
  467. stopTaskProcessing() {
  468. return this.appService.stopTaskProcessing();
  469. }
  470. // Task management endpoints
  471. @Get('tasks')
  472. getTasks() {
  473. return this.appService.getAllTasks();
  474. }
  475. @Get('tasks/:id')
  476. getTask(@Param('id') id: string) {
  477. return this.appService.getTaskById(parseInt(id));
  478. }
  479. @Put('tasks/:id')
  480. updateTask(@Param('id') id: string, @Body() updates: any) {
  481. return this.appService.updateTask(parseInt(id), updates);
  482. }
  483. @Post('tasks')
  484. createTask(@Body() taskData: any) {
  485. return this.appService.createTask(taskData);
  486. }
  487. }