From 0200b928609585d35fdb479fc5a3c8621fb4c2c6 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 29 Oct 2022 19:12:02 -0700 Subject: [PATCH] Update scanner (server) --- server/controllers/index.ts | 16 +- server/controllers/servers.controller.ts | 50 ++- server/controllers/tasks.controller.ts | 108 +++++++ server/helpers/api-model.ts | 49 +++ server/package-lock.json | 292 +++++++++++++++++- server/package.json | 1 + .../migration.sql | 16 + server/prisma/schema.prisma | 25 +- server/prisma/seed.ts | 40 ++- server/queue/jellyfin/jellyfin.scanner.ts | 5 + server/queue/navidrome/navidrome.api.ts | 12 +- server/queue/navidrome/navidrome.scanner.ts | 76 +++-- server/queue/queues/scanner.queue.ts | 16 +- server/queue/subsonic/subsonic.scanner.ts | 35 ++- server/routes/servers.route.ts | 10 +- server/routes/tasks.route.ts | 38 ++- server/server.ts | 18 +- server/services/servers.service.ts | 86 ++---- server/sockets/index.ts | 15 + server/validations/index.ts | 18 +- server/validations/tasks.validation.ts | 28 ++ 21 files changed, 777 insertions(+), 177 deletions(-) create mode 100644 server/controllers/tasks.controller.ts create mode 100644 server/prisma/migrations/20221029082025_add_user_to_task/migration.sql create mode 100644 server/sockets/index.ts create mode 100644 server/validations/tasks.validation.ts diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 88e2aeebf..16c2b1896 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts @@ -1,10 +1,11 @@ -import { albumArtistsController } from './album-artists.controller'; -import { albumsController } from './albums.controller'; -import { artistsController } from './artists.controller'; -import { authController } from './auth.controller'; -import { serversController } from './servers.controller'; -import { songsController } from './songs.controller'; -import { usersController } from './users.controller'; +import { albumArtistsController } from '@controllers/album-artists.controller'; +import { albumsController } from '@controllers/albums.controller'; +import { artistsController } from '@controllers/artists.controller'; +import { authController } from '@controllers/auth.controller'; +import { serversController } from '@controllers/servers.controller'; +import { songsController } from '@controllers/songs.controller'; +import { tasksController } from '@controllers/tasks.controller'; +import { usersController } from '@controllers/users.controller'; export const controller = { albumArtists: albumArtistsController, @@ -13,5 +14,6 @@ export const controller = { auth: authController, servers: serversController, songs: songsController, + tasks: tasksController, users: usersController, }; diff --git a/server/controllers/servers.controller.ts b/server/controllers/servers.controller.ts index ebd86f102..e74c0df1f 100644 --- a/server/controllers/servers.controller.ts +++ b/server/controllers/servers.controller.ts @@ -1,6 +1,6 @@ import { ServerType } from '@prisma/client'; import { Response } from 'express'; -import { ApiSuccess, getSuccessResponse } from '@/utils'; +import { ApiError, ApiSuccess, getSuccessResponse } from '@/utils'; import { toApiModel } from '@helpers/api-model'; import { service } from '@services/index'; import { TypedRequest, validation } from '@validations/index'; @@ -109,7 +109,7 @@ const refreshServer = async ( return res.status(success.statusCode).json(getSuccessResponse(success)); }; -const scanServer = async ( +const fullScanServer = async ( req: TypedRequest, res: Response ) => { @@ -118,15 +118,56 @@ const scanServer = async ( // TODO: Check that server is accessible first with the saved token, otherwise throw error - const data = await service.servers.fullScan({ + const scansInProgress = await service.servers.findScanInProgress({ + serverId, + }); + + if (scansInProgress.length > 0) { + throw ApiError.badRequest('Scan already in progress'); + } + + const io = req.app.get('socketio'); + await io.emit('task:started'); + + const data = await service.servers.fullScan(req.authUser, { id: serverId, serverFolderId, }); + // return res.status(200).json({ data: null }); const success = ApiSuccess.ok({ data }); return res.status(success.statusCode).json(getSuccessResponse(success)); }; +const quickScanServer = async ( + req: TypedRequest, + res: Response +) => { + const { serverId } = req.params; + const { serverFolderId } = req.body; + + // TODO: Check that server is accessible first with the saved token, otherwise throw error + + const scansInProgress = await service.servers.findScanInProgress({ + serverId, + }); + + if (scansInProgress.length > 0) { + throw ApiError.badRequest('Scan already in progress'); + } + + const io = req.app.get('socketio'); + await io.emit('task:started'); + + // await service.servers.fullScan({ + // id: serverId, + // serverFolderId, + // }); + + const success = ApiSuccess.ok({ data: null }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + const createServerUrl = async ( req: TypedRequest, res: Response @@ -228,9 +269,10 @@ export const serversController = { disableServerUrl, enableServerFolder, enableServerUrl, + fullScanServer, getServerDetail, getServerList, + quickScanServer, refreshServer, - scanServer, updateServer, }; diff --git a/server/controllers/tasks.controller.ts b/server/controllers/tasks.controller.ts new file mode 100644 index 000000000..8a11c5c6b --- /dev/null +++ b/server/controllers/tasks.controller.ts @@ -0,0 +1,108 @@ +import { Request, Response } from 'express'; +import { queue } from '@/queue/queues'; +import { toApiModel } from '@helpers/api-model'; +import { prisma } from '@lib/prisma'; +import { ApiSuccess } from '@utils/api-success'; +import { getSuccessResponse } from '@utils/get-success-response'; +import { validation } from '@validations/index'; +import { TypedRequest } from '@validations/shared.validation'; +import { SortOrder } from '../types/types'; + +const getActiveTasks = async (_req: Request, res: Response) => { + const tasks = await prisma.task.findMany({ + include: { + server: true, + user: true, + }, + orderBy: { + createdAt: SortOrder.ASC, + }, + where: { + completed: false, + isError: false, + }, + }); + + if (queue.scanner.length === 0) { + await prisma.task.updateMany({ + data: { completed: true, isError: true, message: 'Task not found' }, + where: { completed: false }, + }); + } + + const success = ApiSuccess.ok({ data: toApiModel.tasks({ items: tasks }) }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const cancelAllTasks = async ( + _req: TypedRequest, + res: Response +) => { + const runningTasks = await prisma.task.findMany({ + include: { + server: true, + user: true, + }, + where: { + completed: false, + isError: false, + }, + }); + + for (const task of runningTasks) { + queue.scanner.push({ + fn: async () => { + return {}; + }, + id: task.id, + }); + } + + await prisma.task.updateMany({ + data: { + completed: true, + message: 'Task was cancelled by user', + }, + where: { completed: false }, + }); + + const success = ApiSuccess.noContent({ data: null }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +const cancelTaskById = async ( + req: TypedRequest, + res: Response +) => { + const { taskId } = req.params; + + const task = await prisma.task.update({ + data: { + completed: true, + message: 'Task was cancelled by user', + }, + include: { + server: true, + user: true, + }, + where: { id: taskId }, + }); + + queue.scanner.push({ + fn: async () => { + return {}; + }, + id: taskId, + }); + + const success = ApiSuccess.ok({ + data: toApiModel.tasks({ items: [task] })[0], + }); + return res.status(success.statusCode).json(getSuccessResponse(success)); +}; + +export const tasksController = { + cancelAllTasks, + cancelTaskById, + getActiveTasks, +}; diff --git a/server/helpers/api-model.ts b/server/helpers/api-model.ts index 0484c56b2..5b1590c3f 100644 --- a/server/helpers/api-model.ts +++ b/server/helpers/api-model.ts @@ -18,6 +18,7 @@ import { ServerUrl, Song, SongRating, + Task, User, UserServerUrl, } from '@prisma/client'; @@ -516,6 +517,17 @@ const servers = ( ); }; +const relatedServers = (items: Server[]) => { + const result = items.map((item) => ({ + id: item.id, + name: item.name, + type: item.type, + url: item.url, + })); + + return result || []; +}; + const relatedServerFolderPermissions = (items: ServerFolderPermission[]) => { return items.map((item) => { return { @@ -578,9 +590,46 @@ const users = ( ); }; +const relatedUsers = (items: User[]) => { + const result = items.map((item) => ({ + enabled: item.enabled, + id: item.id, + isAdmin: item.isAdmin, + username: item.username, + })); + + return result || []; +}; + +type DbTask = Task & DbTaskInclude; + +type DbTaskInclude = { + server: Server; + user: User; +}; + +const tasks = (options: { items: DbTask[] | any[] }) => { + const { items } = options; + + const result = items.map((item) => ({ + createdAt: item.createdAt, + id: item.id, + isCompleted: item.completed, + isError: item.isError, + message: item.message, + server: item.server ? relatedServers([item.server])[0] : null, + type: item.type, + updatedAt: item.updatedAt, + user: item.user ? relatedUsers([item.user])[0] : null, + })); + + return result; +}; + export const toApiModel = { albums, servers, songs, + tasks, users, }; diff --git a/server/package-lock.json b/server/package-lock.json index c9cf20d58..70bb6f13d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "feishin-server", "version": "1.0.0-alpha1", - "license": "ISC", + "license": "GPL-3.0", "dependencies": { "@prisma/client": "^4.5.0", "axios": "^0.27.2", @@ -25,6 +25,7 @@ "passport": "^0.4.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "socket.io": "^4.5.3", "zod": "^3.19.1" }, "devDependencies": { @@ -1552,6 +1553,11 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "node_modules/@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -1665,6 +1671,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "node_modules/@types/cookie-parser": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", @@ -1677,8 +1688,7 @@ "node_modules/@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, "node_modules/@types/eslint": { "version": "8.4.8", @@ -1827,8 +1837,7 @@ "node_modules/@types/node": { "version": "18.8.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.4.tgz", - "integrity": "sha512-WdlVphvfR/GJCLEMbNA8lJ0lhFNBj4SW3O+O5/cEGw9oYrv0al9zTwuQsq+myDUXgNx2jgBynoVgZ2MMJ6pbow==", - "dev": true + "integrity": "sha512-WdlVphvfR/GJCLEMbNA8lJ0lhFNBj4SW3O+O5/cEGw9oYrv0al9zTwuQsq+myDUXgNx2jgBynoVgZ2MMJ6pbow==" }, "node_modules/@types/passport": { "version": "1.0.7", @@ -2866,6 +2875,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -3539,6 +3556,55 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", + "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", + "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/enhanced-resolve": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", @@ -8600,6 +8666,81 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/socket.io": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", + "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.2.0", + "socket.io-adapter": "~2.4.0", + "socket.io-parser": "~4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", + "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", + "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9531,6 +9672,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -10791,6 +10952,11 @@ "@sinonjs/commons": "^1.7.0" } }, + "@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, "@tsconfig/node10": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", @@ -10903,6 +11069,11 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "@types/cookie-parser": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", @@ -10915,8 +11086,7 @@ "@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", - "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", - "dev": true + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" }, "@types/eslint": { "version": "8.4.8", @@ -11065,8 +11235,7 @@ "@types/node": { "version": "18.8.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.4.tgz", - "integrity": "sha512-WdlVphvfR/GJCLEMbNA8lJ0lhFNBj4SW3O+O5/cEGw9oYrv0al9zTwuQsq+myDUXgNx2jgBynoVgZ2MMJ6pbow==", - "dev": true + "integrity": "sha512-WdlVphvfR/GJCLEMbNA8lJ0lhFNBj4SW3O+O5/cEGw9oYrv0al9zTwuQsq+myDUXgNx2jgBynoVgZ2MMJ6pbow==" }, "@types/passport": { "version": "1.0.7", @@ -11876,6 +12045,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", @@ -12381,6 +12555,43 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "engine.io": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", + "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "engine.io-parser": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz", + "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==" + }, "enhanced-resolve": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", @@ -16105,6 +16316,63 @@ "is-fullwidth-code-point": "^3.0.0" } }, + "socket.io": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz", + "integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.2.0", + "socket.io-adapter": "~2.4.0", + "socket.io-parser": "~4.2.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "socket.io-adapter": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz", + "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==" + }, + "socket.io-parser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.1.tgz", + "integrity": "sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16799,6 +17067,12 @@ "signal-exit": "^3.0.7" } }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/server/package.json b/server/package.json index 09f55638e..e857e9792 100644 --- a/server/package.json +++ b/server/package.json @@ -73,6 +73,7 @@ "passport": "^0.4.1", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", + "socket.io": "^4.5.3", "zod": "^3.19.1" } } diff --git a/server/prisma/migrations/20221029082025_add_user_to_task/migration.sql b/server/prisma/migrations/20221029082025_add_user_to_task/migration.sql new file mode 100644 index 000000000..a648499dc --- /dev/null +++ b/server/prisma/migrations/20221029082025_add_user_to_task/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `name` on the `Task` table. All the data in the column will be lost. + - You are about to drop the column `progress` on the `Task` table. All the data in the column will be lost. + - Made the column `isError` on table `Task` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Task" DROP COLUMN "name", +DROP COLUMN "progress", +ADD COLUMN "userId" UUID, +ALTER COLUMN "isError" SET NOT NULL; + +-- AddForeignKey +ALTER TABLE "Task" ADD CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 5ea657b39..3129243f2 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -75,12 +75,12 @@ model User { serverFolderPermissions ServerFolderPermission[] serverPermissions ServerPermission[] - // serverCredentials ServerCredential[] albumArtistFavorites AlbumArtistFavorite[] artistFavorites ArtistFavorite[] albumFavorites AlbumFavorite[] songFavorites SongFavorite[] userServerUrls UserServerUrl[] + tasks Task[] } model History { @@ -112,26 +112,10 @@ model Server { serverUrls ServerUrl[] folders Folder[] serverPermissions ServerPermission[] - // serverCredentials ServerCredential[] tasks Task[] userServerUrls UserServerUrl[] } -// model ServerCredential { -// id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid -// username String -// enabled Boolean @default(false) -// credential String -// createdAt DateTime @default(now()) -// updatedAt DateTime @updatedAt - -// server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) -// serverId String @db.Uuid - -// user User @relation(fields: [userId], references: [id], onDelete: Cascade) -// userId String @db.Uuid -// } - model Folder { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String @@ -518,15 +502,16 @@ model Song { model Task { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String type TaskType message String? - progress String? completed Boolean @default(false) - isError Boolean? @default(false) + isError Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt server Server @relation(fields: [serverId], references: [id], onDelete: Cascade) serverId String @db.Uuid + + user User? @relation(fields: [userId], references: [id]) + userId String? @db.Uuid } diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index e8f283955..388348563 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -1,24 +1,34 @@ -/* eslint-disable promise/catch-or-return */ -import { PrismaClient } from '@prisma/client'; -import bcrypt from 'bcryptjs'; +import { PrismaClient, Prisma } from '@prisma/client'; import { randomString } from '../utils'; const prisma = new PrismaClient(); async function main() { - const hashedPassword = await bcrypt.hash('admin', 12); + const hashedPassword = + '$2y$12$icIH42ono1yTBypZ34V/PuDMXIbMD04GtSB6pgYpcwbjjIvujzv2y'; - await prisma.user.upsert({ - create: { - deviceId: `admin_${randomString(10)}`, - enabled: true, - isAdmin: true, - password: hashedPassword, - username: 'admin', - }, - update: {}, - where: { username: 'admin' }, - }); + let error; + do { + try { + await prisma.user.upsert({ + create: { + deviceId: `admin_${randomString(10)}`, + enabled: true, + isAdmin: true, + password: hashedPassword, + username: 'admin', + }, + update: {}, + where: { username: 'admin' }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientInitializationError) { + error = 'retry'; + } + + error = undefined; + } + } while (error === 'retry'); } main() diff --git a/server/queue/jellyfin/jellyfin.scanner.ts b/server/queue/jellyfin/jellyfin.scanner.ts index 4e9456b6e..138a0d3df 100644 --- a/server/queue/jellyfin/jellyfin.scanner.ts +++ b/server/queue/jellyfin/jellyfin.scanner.ts @@ -478,6 +478,11 @@ const scanAll = async ( await scanAlbums(server, serverFolder, task); await scanSongs(server, serverFolder, task); await checkDeleted(server, serverFolder, task); + + await prisma.serverFolder.update({ + data: { lastScannedAt: new Date() }, + where: { id: serverFolder.id }, + }); } return { task }; diff --git a/server/queue/navidrome/navidrome.api.ts b/server/queue/navidrome/navidrome.api.ts index 3993aff08..7d83c7ffe 100644 --- a/server/queue/navidrome/navidrome.api.ts +++ b/server/queue/navidrome/navidrome.api.ts @@ -30,10 +30,11 @@ const authenticate = async (options: { }; const getGenres = async (server: Server, params?: NDGenreListParams) => { + const [ndToken] = server.token.split('||'); const { data } = await api.get( `${server.url}/api/genre`, { - headers: { 'x-nd-authorization': `Bearer ${server.token}` }, + headers: { 'x-nd-authorization': `Bearer ${ndToken}` }, params, } ); @@ -42,10 +43,11 @@ const getGenres = async (server: Server, params?: NDGenreListParams) => { }; const getArtists = async (server: Server, params?: NDGenreListParams) => { + const [ndToken] = server.token.split('||'); const { data } = await api.get( `${server.url}/api/artist`, { - headers: { 'x-nd-authorization': `Bearer ${server.token}` }, + headers: { 'x-nd-authorization': `Bearer ${ndToken}` }, params, } ); @@ -54,10 +56,11 @@ const getArtists = async (server: Server, params?: NDGenreListParams) => { }; const getAlbums = async (server: Server, params?: NDAlbumListParams) => { + const [ndToken] = server.token.split('||'); const { data } = await api.get( `${server.url}/api/album`, { - headers: { 'x-nd-authorization': `Bearer ${server.token}` }, + headers: { 'x-nd-authorization': `Bearer ${ndToken}` }, params, } ); @@ -66,8 +69,9 @@ const getAlbums = async (server: Server, params?: NDAlbumListParams) => { }; const getSongs = async (server: Server, params?: NDSongListParams) => { + const [ndToken] = server.token.split('||'); const { data } = await api.get(`${server.url}/api/song`, { - headers: { 'x-nd-authorization': `Bearer ${server.token}` }, + headers: { 'x-nd-authorization': `Bearer ${ndToken}` }, params, }); diff --git a/server/queue/navidrome/navidrome.scanner.ts b/server/queue/navidrome/navidrome.scanner.ts index 72250132f..d7a59299e 100644 --- a/server/queue/navidrome/navidrome.scanner.ts +++ b/server/queue/navidrome/navidrome.scanner.ts @@ -37,8 +37,14 @@ export const scanGenres = async (server: Server, task: Task) => { export const scanAlbumArtists = async ( server: Server, - serverFolder: ServerFolder + serverFolder: ServerFolder, + task: Task ) => { + await prisma.task.update({ + data: { message: 'Scanning artists' }, + where: { id: task.id }, + }); + const artists = await navidromeApi.getArtists(server); const externalsCreateMany = artists @@ -101,8 +107,14 @@ export const scanAlbumArtists = async ( export const scanAlbums = async ( server: Server, - serverFolder: ServerFolder + serverFolder: ServerFolder, + task: Task ) => { + await prisma.task.update({ + data: { message: 'Scanning artists' }, + where: { id: task.id }, + }); + let start = 0; let count = 5000; do { @@ -153,26 +165,38 @@ export const scanAlbums = async ( } } - const artistsConnect = validArtistIds.map((id) => ({ - uniqueArtistId: { - remoteId: id, - serverId: server.id, - }, - })); + // const artistsConnect = validArtistIds.map((id) => ({ + // uniqueArtistId: { + // remoteId: id, + // serverId: server.id, + // }, + // })); - const albumArtistConnect = album.artistId + const aaConnect = []; + const albumArtistConnect = album.albumArtistId ? { uniqueAlbumArtistId: { - remoteId: album.artistId, + remoteId: album.albumArtistId, serverId: server.id, }, } : undefined; + aaConnect.push( + ...validArtistIds.map((id) => ({ + uniqueAlbumArtistId: { + remoteId: id, + serverId: server.id, + }, + })) + ); + + albumArtistConnect && aaConnect.push(albumArtistConnect); + await prisma.album.upsert({ create: { - albumArtists: { connect: albumArtistConnect }, - artists: { connect: artistsConnect }, + albumArtists: { connect: aaConnect }, + // artists: { connect: artistsConnect }, deleted: false, genres: { connect: genresConnect }, images: { connect: imagesConnect }, @@ -188,8 +212,8 @@ export const scanAlbums = async ( sortName: album.name, }, update: { - albumArtists: { connect: albumArtistConnect }, - artists: { connect: artistsConnect }, + albumArtists: { connect: aaConnect }, + // artists: { connect: artistsConnect }, deleted: false, genres: { connect: genresConnect }, images: { connect: imagesConnect }, @@ -218,7 +242,16 @@ export const scanAlbums = async ( } while (count === CHUNK_SIZE); }; -const scanSongs = async (server: Server, serverFolder: ServerFolder) => { +const scanSongs = async ( + server: Server, + serverFolder: ServerFolder, + task: Task +) => { + await prisma.task.update({ + data: { message: 'Scanning artists' }, + where: { id: task.id }, + }); + let start = 0; let count = 5000; do { @@ -313,7 +346,7 @@ const scanSongs = async (server: Server, serverFolder: ServerFolder) => { } for (const folder of createdFolders) { - if (folder.parentId) break; + if (folder?.parentId || !folder) break; const pathSplit = folder.path.split('/'); const parentPath = pathSplit.slice(0, pathSplit.length - 1).join('/'); @@ -359,9 +392,14 @@ const scanAll = async ( for (const serverFolder of serverFolders) { await scanGenres(server, task); - await scanAlbumArtists(server, serverFolder); - await scanAlbums(server, serverFolder); - await scanSongs(server, serverFolder); + await scanAlbumArtists(server, serverFolder, task); + await scanAlbums(server, serverFolder, task); + await scanSongs(server, serverFolder, task); + + await prisma.serverFolder.update({ + data: { lastScannedAt: new Date() }, + where: { id: serverFolder.id }, + }); } return { task }; diff --git a/server/queue/queues/scanner.queue.ts b/server/queue/queues/scanner.queue.ts index a0462581c..7f741520b 100644 --- a/server/queue/queues/scanner.queue.ts +++ b/server/queue/queues/scanner.queue.ts @@ -8,7 +8,7 @@ interface QueueTask { task: Task; } -export const scannerQueue: Queue = new Queue( +export const scannerQueue: Queue | any = new Queue( async (task: QueueTask, cb: any) => { const result = await task.fn(); return cb(null, result); @@ -18,26 +18,20 @@ export const scannerQueue: Queue = new Queue( cancelIfRunning: true, concurrent: 1, filo: false, - maxRetries: 5, - maxTimeout: 600000, - retryDelay: 2000, } ); -scannerQueue.on('task_finish', async (taskId) => { +scannerQueue.on('task_finish', async (taskId: string) => { await prisma.task.update({ data: { completed: true, isError: false, - progress: null, }, where: { id: taskId }, }); }); -scannerQueue.on('task_failed', async (taskId, errorMessage) => { - const dbTaskId = taskId.split('(')[1].split(')')[0]; - +scannerQueue.on('task_failed', async (taskId: string, errorMessage: string) => { console.log('errorMessage', errorMessage); await prisma.task.update({ data: { @@ -45,13 +39,13 @@ scannerQueue.on('task_failed', async (taskId, errorMessage) => { isError: true, message: errorMessage, }, - where: { id: dbTaskId }, + where: { id: taskId }, }); }); scannerQueue.on('drain', async () => { await prisma.task.updateMany({ - data: { completed: true, progress: null }, + data: { completed: true }, where: { completed: false }, }); }); diff --git a/server/queue/subsonic/subsonic.scanner.ts b/server/queue/subsonic/subsonic.scanner.ts index 5451b29bf..cf1c3d26f 100644 --- a/server/queue/subsonic/subsonic.scanner.ts +++ b/server/queue/subsonic/subsonic.scanner.ts @@ -26,8 +26,14 @@ export const scanGenres = async (server: Server, task: Task) => { export const scanAlbumArtists = async ( server: Server, - serverFolder: ServerFolder + serverFolder: ServerFolder, + task: Task ) => { + await prisma.task.update({ + data: { message: 'Scanning artists' }, + where: { id: task.id }, + }); + const artists = await subsonicApi.getArtists(server, serverFolder.remoteId); for (const artist of artists) { @@ -58,8 +64,14 @@ export const scanAlbumArtists = async ( export const scanAlbums = async ( server: Server, - serverFolder: ServerFolder + serverFolder: ServerFolder, + task: Task ) => { + await prisma.task.update({ + data: { message: 'Scanning albums' }, + where: { id: task.id }, + }); + const albums = await subsonicApi.getAlbums(server, { musicFolderId: serverFolder.id, offset: 0, @@ -241,8 +253,14 @@ const throttledAlbumFetch = throttle( export const scanAlbumDetail = async ( server: Server, - serverFolder: ServerFolder + serverFolder: ServerFolder, + task: Task ) => { + await prisma.task.update({ + data: { message: 'Scanning songs' }, + where: { id: task.id }, + }); + const promises = []; const dbAlbums = await prisma.album.findMany({ where: { @@ -271,9 +289,14 @@ const scanAll = async ( for (const serverFolder of serverFolders) { await scanGenres(server, task); - await scanAlbumArtists(server, serverFolder); - await scanAlbums(server, serverFolder); - await scanAlbumDetail(server, serverFolder); + await scanAlbumArtists(server, serverFolder, task); + await scanAlbums(server, serverFolder, task); + await scanAlbumDetail(server, serverFolder, task); + + await prisma.serverFolder.update({ + data: { lastScannedAt: new Date() }, + where: { id: serverFolder.id }, + }); } return { task }; diff --git a/server/routes/servers.route.ts b/server/routes/servers.route.ts index fff7e5947..1a92f22cf 100644 --- a/server/routes/servers.route.ts +++ b/server/routes/servers.route.ts @@ -48,7 +48,15 @@ router .post( validateRequest(validation.servers.scan), authenticateAdmin, - controller.servers.scanServer + controller.servers.quickScanServer + ); + +router + .route('/:serverId/full-scan') + .post( + validateRequest(validation.servers.scan), + authenticateAdmin, + controller.servers.fullScanServer ); router diff --git a/server/routes/tasks.route.ts b/server/routes/tasks.route.ts index f3cdaca0b..8719ba5d5 100644 --- a/server/routes/tasks.route.ts +++ b/server/routes/tasks.route.ts @@ -1,11 +1,39 @@ import express, { Router } from 'express'; +import { controller } from '@controllers/index'; +import { prisma } from '@lib/prisma'; +import { authenticateAdmin } from '@middleware/authenticate-admin'; +import { ApiError } from '@utils/api-error'; +import { validation } from '@validations/index'; +import { validateRequest } from '@validations/shared.validation'; export const router: Router = express.Router({ mergeParams: true }); -router.post('/scan', async (_req, res) => { - return res.status(200); +router + .route('/') + .get(validateRequest(validation.tasks.list), controller.tasks.getActiveTasks); + +router + .route('/cancel') + .post( + authenticateAdmin, + validateRequest(validation.tasks.cancelAll), + controller.tasks.cancelAllTasks + ); + +router.param('taskId', async (_req, _res, next, taskId) => { + const task = await prisma.task.findUnique({ where: { id: taskId } }); + + if (!task) { + throw ApiError.notFound('Task not found'); + } + + next(); }); -router.post('/', async (_req, res) => { - return res.status(200).json({}); -}); +router + .route('/:taskId/cancel') + .post( + authenticateAdmin, + validateRequest(validation.tasks.cancel), + controller.tasks.cancelTaskById + ); diff --git a/server/server.ts b/server/server.ts index 28c049e25..f1de4bf9e 100644 --- a/server/server.ts +++ b/server/server.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/order */ import path from 'path'; import cookieParser from 'cookie-parser'; import cors from 'cors'; @@ -6,6 +7,9 @@ import passport from 'passport'; import 'express-async-errors'; import { errorHandler } from '@/middleware'; import { routes } from '@routes/index'; +import { sockets } from '@sockets/index'; +import * as http from 'http'; +import * as socketio from 'socket.io'; require('./lib/passport'); @@ -37,4 +41,16 @@ app.get('/', (_req, res) => { app.use(routes); app.use(errorHandler); -app.listen(9321, () => console.log(`Listening on port ${PORT}`)); +const server = http.createServer(app); +const io = new socketio.Server(server, { + cors: { + credentials: false, + methods: ['GET', 'POST'], + origin: [`http://localhost:4343`, `${process.env.APP_BASE_URL}`], + }, +}); + +app.set('socketio', io); +io.on('connection', (socket) => sockets(socket)); + +server.listen(9321, () => console.log(`Listening on port ${PORT}`)); diff --git a/server/services/servers.service.ts b/server/services/servers.service.ts index 8c5e320d7..865d7de14 100644 --- a/server/services/servers.service.ts +++ b/server/services/servers.service.ts @@ -364,7 +364,22 @@ const refresh = async (options: { id: string }) => { return server; }; -const fullScan = async (options: { id: string; serverFolderId?: string[] }) => { +const findScanInProgress = async (options: { serverId: string }) => { + const tasks = await prisma.task.findMany({ + where: { + OR: [{ type: TaskType.FULL_SCAN }, { type: TaskType.QUICK_SCAN }], + completed: false, + serverId: options.serverId, + }, + }); + + return tasks; +}; + +const fullScan = async ( + user: AuthUser, + options: { id: string; serverFolderId?: string[] } +) => { const { id, serverFolderId } = options; // Only allow scan of enabled folders @@ -392,10 +407,9 @@ const fullScan = async (options: { id: string; serverFolderId?: string[] }) => { const task = await prisma.task.create({ data: { - completed: false, - name: 'Full scan', server: { connect: { id: server.id } }, type: TaskType.FULL_SCAN, + user: { connect: { id: user.id } }, }, }); @@ -411,7 +425,7 @@ const fullScan = async (options: { id: string; serverFolderId?: string[] }) => { await navidrome.scanner.scanAll(server, serverFolders, task); } - return {}; + return task; }; const findServerUrlById = async (options: { id: string }) => { @@ -422,69 +436,6 @@ const findServerUrlById = async (options: { id: string }) => { return serverUrl; }; -// const findCredentialById = async (options: { id: string }) => { -// const credential = await prisma.serverCredential.findUnique({ -// where: { id: options.id }, -// }); - -// if (!credential) { -// throw ApiError.notFound('Credential not found.'); -// } - -// return credential; -// }; - -// const createCredential = async (options: { -// credential: string; -// serverId: string; -// userId: string; -// username: string; -// }) => { -// const { credential, serverId, userId, username } = options; - -// const serverCredential = await prisma.serverCredential.create({ -// data: { -// credential, -// serverId, -// userId, -// username, -// }, -// }); - -// return serverCredential; -// }; - -// const deleteCredentialById = async (options: { id: string }) => { -// await prisma.serverCredential.delete({ -// where: { id: options.id }, -// }); -// }; - -// const enableCredentialById = async (options: { id: string }) => { -// const serverCredential = await prisma.serverCredential.update({ -// data: { enabled: true }, -// where: { id: options.id }, -// }); - -// const { id, userId, serverId } = serverCredential; - -// await prisma.serverCredential.updateMany({ -// data: { enabled: false }, -// where: { AND: [{ serverId, userId }, { NOT: { id } }] }, -// }); - -// return serverCredential; -// }; - -// const disableCredentialById = async (options: { id: string }) => { -// const serverCredential = await prisma.serverCredential.update({ -// data: { enabled: false }, -// where: { id: options.id }, -// }); - -// return serverCredential; -// }; - const createUrl = async (options: { serverId: string; url: string }) => { const { serverId, url } = options; @@ -589,6 +540,7 @@ export const serversService = { findById, findFolderById, findMany, + findScanInProgress, findServerUrlById, findUrlById, fullScan, diff --git a/server/sockets/index.ts b/server/sockets/index.ts new file mode 100644 index 000000000..d7e93a574 --- /dev/null +++ b/server/sockets/index.ts @@ -0,0 +1,15 @@ +import { Socket } from 'socket.io'; + +export const sockets = (socket: Socket) => { + socket.broadcast.emit('user:connected', { + userID: socket.id, + username: socket.handshake.query.username, + }); + + socket.on('disconnect', () => { + socket.broadcast.emit('user:disconnected', { + userID: socket.id, + username: socket.handshake.query.username, + }); + }); +}; diff --git a/server/validations/index.ts b/server/validations/index.ts index 5264778b1..ad5520046 100644 --- a/server/validations/index.ts +++ b/server/validations/index.ts @@ -1,12 +1,13 @@ -import { albumArtistsValidation } from './album-artists.validation'; -import { albumsValidation } from './albums.validation'; -import { artistsValidation } from './artists.validation'; -import { authValidation } from './auth.validation'; -import { serversValidation } from './servers.validation'; -import { songsValidation } from './songs.validation'; -import { usersValidation } from './users.validation'; +import { albumArtistsValidation } from '@validations/album-artists.validation'; +import { albumsValidation } from '@validations/albums.validation'; +import { artistsValidation } from '@validations/artists.validation'; +import { authValidation } from '@validations/auth.validation'; +import { serversValidation } from '@validations/servers.validation'; +import { songsValidation } from '@validations/songs.validation'; +import { tasksValidation } from '@validations/tasks.validation'; +import { usersValidation } from '@validations/users.validation'; -export { validateRequest, TypedRequest } from './shared.validation'; +export { validateRequest, TypedRequest } from '@validations/shared.validation'; export const validation = { albumArtists: albumArtistsValidation, @@ -15,5 +16,6 @@ export const validation = { auth: authValidation, servers: serversValidation, songs: songsValidation, + tasks: tasksValidation, users: usersValidation, }; diff --git a/server/validations/tasks.validation.ts b/server/validations/tasks.validation.ts new file mode 100644 index 000000000..dfcb4e7a0 --- /dev/null +++ b/server/validations/tasks.validation.ts @@ -0,0 +1,28 @@ +import { z } from 'zod'; +import { idValidation } from '@validations/shared.validation'; + +const list = { + body: z.object({}), + params: z.object({}), + query: z.object({}), +}; + +const cancelAll = { + body: z.object({}), + params: z.object({}), + query: z.object({}), +}; + +const cancel = { + body: z.object({}), + params: z.object({ + ...idValidation('taskId'), + }), + query: z.object({}), +}; + +export const tasksValidation = { + cancel, + cancelAll, + list, +};