Update scanner (server)

This commit is contained in:
jeffvli
2022-10-29 19:12:02 -07:00
parent ff6882a6cd
commit 0200b92860
21 changed files with 777 additions and 177 deletions
+9 -7
View File
@@ -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,
};
+46 -4
View File
@@ -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<typeof validation.servers.scan>,
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<typeof validation.servers.scan>,
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<typeof validation.servers.createUrl>,
res: Response
@@ -228,9 +269,10 @@ export const serversController = {
disableServerUrl,
enableServerFolder,
enableServerUrl,
fullScanServer,
getServerDetail,
getServerList,
quickScanServer,
refreshServer,
scanServer,
updateServer,
};
+108
View File
@@ -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<typeof validation.tasks.cancelAll>,
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<typeof validation.tasks.cancel>,
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,
};
+49
View File
@@ -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,
};
+283 -9
View File
@@ -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",
+1
View File
@@ -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"
}
}
@@ -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;
+5 -20
View File
@@ -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
}
+25 -15
View File
@@ -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()
@@ -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 };
+8 -4
View File
@@ -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<NDGenreListResponse>(
`${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<NDArtistListResponse>(
`${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<NDAlbumListResponse>(
`${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<NDSongListResponse>(`${server.url}/api/song`, {
headers: { 'x-nd-authorization': `Bearer ${server.token}` },
headers: { 'x-nd-authorization': `Bearer ${ndToken}` },
params,
});
+57 -19
View File
@@ -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 };
+5 -11
View File
@@ -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 },
});
});
+29 -6
View File
@@ -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 };
+9 -1
View File
@@ -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
+33 -5
View File
@@ -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
);
+17 -1
View File
@@ -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}`));
+19 -67
View File
@@ -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,
+15
View File
@@ -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,
});
});
};
+10 -8
View File
@@ -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,
};
+28
View File
@@ -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,
};