mirror of
https://github.com/jeffvli/feishin.git
synced 2026-08-12 07:12:58 +02:00
Redo server functionality
This commit is contained in:
@@ -1,41 +1,45 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { z } from 'zod';
|
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||||
import { albumArtistsService } from '../services';
|
import { service } from '@services/index';
|
||||||
import {
|
import { validation, TypedRequest } from '@validations/index';
|
||||||
getSuccessResponse,
|
|
||||||
idValidation,
|
|
||||||
paginationValidation,
|
|
||||||
validateRequest,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
const getAlbumArtists = async (req: Request, res: Response) => {
|
|
||||||
validateRequest(req, {
|
|
||||||
query: z.object({
|
|
||||||
...paginationValidation,
|
|
||||||
serverFolderIds: z.string().min(1),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const getList = async (req: Request, res: Response) => {
|
||||||
const { take, skip, serverFolderIds } = req.query;
|
const { take, skip, serverFolderIds } = req.query;
|
||||||
const data = await albumArtistsService.findMany(req, {
|
const albumArtists = await service.albumArtists.findMany(req, {
|
||||||
serverFolderIds: String(serverFolderIds),
|
serverFolderIds: String(serverFolderIds),
|
||||||
skip: Number(skip),
|
skip: Number(skip),
|
||||||
take: Number(take),
|
take: Number(take),
|
||||||
user: req.auth,
|
user: req.authUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
const success = ApiSuccess.ok({
|
||||||
|
data: albumArtists.data,
|
||||||
|
paginationItems: {
|
||||||
|
skip: Number(skip),
|
||||||
|
take: Number(take),
|
||||||
|
totalEntries: albumArtists.totalEntries,
|
||||||
|
url: req.originalUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbumArtistById = async (req: Request, res: Response) => {
|
const getDetail = async (
|
||||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
req: TypedRequest<typeof validation.albumArtists.detail>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const data = await albumArtistsService.findById({
|
const albumArtist = await service.albumArtists.findById({
|
||||||
id: Number(id),
|
id,
|
||||||
user: req.auth,
|
user: req.authUser,
|
||||||
});
|
});
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
|
||||||
|
const success = ApiSuccess.ok({ data: albumArtist });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const albumArtistsController = { getAlbumArtistById, getAlbumArtists };
|
export const albumArtistsController = {
|
||||||
|
getDetail,
|
||||||
|
getList,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,60 +1,102 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { z } from 'zod';
|
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||||
import { AlbumSort } from '../helpers/albums.helpers';
|
import { toApiModel } from '@helpers/api-model';
|
||||||
import { albumsService } from '../services';
|
import { service } from '@services/index';
|
||||||
import { SortOrder } from '../types/types';
|
import { TypedRequest, validation } from '@validations/index';
|
||||||
import {
|
|
||||||
getSuccessResponse,
|
|
||||||
idValidation,
|
|
||||||
paginationValidation,
|
|
||||||
validateRequest,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
const getAlbumById = async (req: Request, res: Response) => {
|
const getDetail = async (
|
||||||
validateRequest(req, {
|
req: TypedRequest<typeof validation.albums.detail>,
|
||||||
params: z.object({ ...idValidation }),
|
res: Response
|
||||||
query: z.object({ serverUrls: z.optional(z.string().min(1)) }),
|
) => {
|
||||||
|
const { albumId } = req.params;
|
||||||
|
|
||||||
|
const album = await service.albums.findById(req.authUser, { id: albumId });
|
||||||
|
|
||||||
|
const success = ApiSuccess.ok({
|
||||||
|
data: toApiModel.albums({ items: [album], user: req.authUser })[0],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { id } = req.params;
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
const { serverUrls } = req.query;
|
|
||||||
const data = await albumsService.findById({
|
|
||||||
id: Number(id),
|
|
||||||
serverUrls: serverUrls && String(serverUrls),
|
|
||||||
user: req.auth,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAlbums = async (req: Request, res: Response) => {
|
const getList = async (
|
||||||
validateRequest(req, {
|
req: TypedRequest<typeof validation.albums.list>,
|
||||||
query: z.object({
|
res: Response
|
||||||
...paginationValidation,
|
) => {
|
||||||
orderBy: z.nativeEnum(SortOrder),
|
const { serverId } = req.params;
|
||||||
serverFolderIds: z.optional(z.string().min(1)),
|
const { take, skip, serverUrlId } = req.query;
|
||||||
serverUrls: z.optional(z.string().min(1)),
|
|
||||||
sortBy: z.nativeEnum(AlbumSort),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { take, serverFolderIds, serverUrls, sortBy, orderBy, skip } =
|
const albums = await service.albums.findMany({
|
||||||
req.query;
|
...req.query,
|
||||||
|
serverId,
|
||||||
const data = await albumsService.findMany(req, {
|
|
||||||
orderBy: orderBy as SortOrder,
|
|
||||||
serverFolderIds: serverFolderIds && String(serverFolderIds),
|
|
||||||
serverUrls: serverUrls && String(serverUrls),
|
|
||||||
skip: Number(skip),
|
skip: Number(skip),
|
||||||
sortBy: sortBy as AlbumSort,
|
|
||||||
take: Number(take),
|
take: Number(take),
|
||||||
user: req.auth,
|
user: req.authUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
const serverUrl = serverUrlId
|
||||||
|
? await service.servers.findServerUrlById({
|
||||||
|
id: serverUrlId,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const success = ApiSuccess.ok({
|
||||||
|
data: toApiModel.albums({
|
||||||
|
items: albums.data,
|
||||||
|
serverUrl: serverUrl?.url,
|
||||||
|
user: req.authUser,
|
||||||
|
}),
|
||||||
|
paginationItems: {
|
||||||
|
skip: Number(skip),
|
||||||
|
take: Number(take),
|
||||||
|
totalEntries: albums.totalEntries,
|
||||||
|
url: req.originalUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDetailSongList = async (
|
||||||
|
req: TypedRequest<typeof validation.albums.list>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
const { take, skip, serverUrlId } = req.query;
|
||||||
|
|
||||||
|
const albums = await service.albums.findMany({
|
||||||
|
...req.query,
|
||||||
|
serverId,
|
||||||
|
skip: Number(skip),
|
||||||
|
take: Number(take),
|
||||||
|
user: req.authUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverUrl = serverUrlId
|
||||||
|
? await service.servers.findServerUrlById({
|
||||||
|
id: serverUrlId,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const success = ApiSuccess.ok({
|
||||||
|
data: toApiModel.albums({
|
||||||
|
items: albums.data,
|
||||||
|
serverUrl: serverUrl?.url,
|
||||||
|
user: req.authUser,
|
||||||
|
}),
|
||||||
|
paginationItems: {
|
||||||
|
skip: Number(skip),
|
||||||
|
take: Number(take),
|
||||||
|
totalEntries: albums.totalEntries,
|
||||||
|
url: req.originalUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const albumsController = {
|
export const albumsController = {
|
||||||
getAlbumById,
|
getDetail,
|
||||||
getAlbums,
|
getDetailSongList,
|
||||||
|
getList,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,41 +1,50 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { z } from 'zod';
|
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||||
import { artistsService } from '../services';
|
import { service } from '@services/index';
|
||||||
import {
|
import { validation, TypedRequest } from '@validations/index';
|
||||||
getSuccessResponse,
|
|
||||||
idValidation,
|
|
||||||
paginationValidation,
|
|
||||||
validateRequest,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
const getArtistById = async (req: Request, res: Response) => {
|
|
||||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
|
||||||
|
|
||||||
|
const getDetail = async (
|
||||||
|
req: TypedRequest<typeof validation.artists.detail>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const data = await artistsService.findById({
|
|
||||||
id: Number(id),
|
const artist = await service.artists.findById({
|
||||||
user: req.auth,
|
id,
|
||||||
|
user: req.authUser,
|
||||||
});
|
});
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
|
||||||
|
const success = ApiSuccess.ok({ data: artist });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getArtists = async (req: Request, res: Response) => {
|
const getList = async (
|
||||||
validateRequest(req, {
|
req: TypedRequest<typeof validation.artists.list>,
|
||||||
query: z.object({
|
res: Response
|
||||||
...paginationValidation,
|
) => {
|
||||||
serverFolderIds: z.string().min(1),
|
const { take, skip, serverFolderId } = req.query;
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { take, skip, serverFolderIds } = req.query;
|
// const artists = await service.artists.findMany(req, {
|
||||||
const data = await artistsService.findMany(req, {
|
// serverFolderIds: String(serverFolderIds),
|
||||||
serverFolderIds: String(serverFolderIds),
|
// skip: Number(skip),
|
||||||
skip: Number(skip),
|
// take: Number(take),
|
||||||
take: Number(take),
|
// user: req.authUser,
|
||||||
user: req.auth,
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
// const success = ApiSuccess.ok({
|
||||||
|
// data: artists,
|
||||||
|
// paginationItems: {
|
||||||
|
// skip: Number(skip),
|
||||||
|
// take: Number(take),
|
||||||
|
// totalEntries,
|
||||||
|
// url: req.originalUrl,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const artistsController = { getArtistById, getArtists };
|
export const artistsController = {
|
||||||
|
getDetail,
|
||||||
|
getList,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { z } from 'zod';
|
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||||
|
import { toApiModel } from '@helpers/api-model';
|
||||||
|
import { service } from '@services/index';
|
||||||
|
import { validation, TypedRequest } from '@validations/index';
|
||||||
import packageJson from '../package.json';
|
import packageJson from '../package.json';
|
||||||
import { authService } from '../services';
|
|
||||||
import { getSuccessResponse, validateRequest } from '../utils';
|
|
||||||
|
|
||||||
const login = async (req: Request, res: Response) => {
|
|
||||||
validateRequest(req, { body: z.object({ username: z.string() }) });
|
|
||||||
|
|
||||||
|
const login = async (
|
||||||
|
req: TypedRequest<typeof validation.auth.login>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
const { username } = req.body;
|
const { username } = req.body;
|
||||||
const { statusCode, data } = await authService.login({ username });
|
const user = await service.auth.login({ username });
|
||||||
|
|
||||||
return res.status(statusCode).json(getSuccessResponse({ data, statusCode }));
|
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = async (req: Request, res: Response) => {
|
const register = async (
|
||||||
validateRequest(req, {
|
req: TypedRequest<typeof validation.auth.register>,
|
||||||
body: z.object({
|
res: Response
|
||||||
password: z.string().min(6).max(255),
|
) => {
|
||||||
username: z.string().min(4).max(26),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
const { statusCode, data } = await authService.register({
|
const user = await service.auth.register({
|
||||||
password,
|
password,
|
||||||
username,
|
username,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(statusCode).json(getSuccessResponse({ data, statusCode }));
|
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async (req: Request, res: Response) => {
|
const logout = async (req: Request, res: Response) => {
|
||||||
const { statusCode, data } = await authService.logout({ user: req.auth });
|
await service.auth.logout({
|
||||||
return res.status(statusCode).json(getSuccessResponse({ data, statusCode }));
|
user: req.authUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
const success = ApiSuccess.noContent({ data: {} });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const ping = async (_req: Request, res: Response) => {
|
const ping = async (_req: Request, res: Response) => {
|
||||||
@@ -48,18 +52,16 @@ const ping = async (_req: Request, res: Response) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = async (req: Request, res: Response) => {
|
const refresh = async (
|
||||||
validateRequest(req, {
|
req: TypedRequest<typeof validation.auth.refresh>,
|
||||||
body: z.object({
|
res: Response
|
||||||
refreshToken: z.string(),
|
) => {
|
||||||
}),
|
const refresh = await service.auth.refresh({
|
||||||
});
|
|
||||||
|
|
||||||
const { data, statusCode } = await authService.refresh({
|
|
||||||
refreshToken: req.body.refreshToken,
|
refreshToken: req.body.refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(statusCode).json(getSuccessResponse({ data, statusCode }));
|
const success = ApiSuccess.ok({ data: refresh });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authController = { login, logout, ping, refresh, register };
|
export const authController = { login, logout, ping, refresh, register };
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
export * from './album-artists.controller';
|
import { albumArtistsController } from './album-artists.controller';
|
||||||
export * from './auth.controller';
|
import { albumsController } from './albums.controller';
|
||||||
export * from './servers.controller';
|
import { artistsController } from './artists.controller';
|
||||||
export * from './users.controller';
|
import { authController } from './auth.controller';
|
||||||
export * from './artists.controller';
|
import { serversController } from './servers.controller';
|
||||||
export * from './albums.controller';
|
import { songsController } from './songs.controller';
|
||||||
|
import { usersController } from './users.controller';
|
||||||
|
|
||||||
|
export const controller = {
|
||||||
|
albumArtists: albumArtistsController,
|
||||||
|
albums: albumsController,
|
||||||
|
artists: artistsController,
|
||||||
|
auth: authController,
|
||||||
|
servers: serversController,
|
||||||
|
songs: songsController,
|
||||||
|
users: usersController,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,72 +1,177 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { z } from 'zod';
|
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||||
import { prisma } from '../lib';
|
import { toApiModel } from '@helpers/api-model';
|
||||||
import { serversService } from '../services';
|
import { service } from '@services/index';
|
||||||
import { getSuccessResponse, idValidation, validateRequest } from '../utils';
|
import { TypedRequest, validation } from '@validations/index';
|
||||||
|
|
||||||
const getServerById = async (req: Request, res: Response) => {
|
const getServerDetail = async (
|
||||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
req: TypedRequest<typeof validation.servers.detail>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
const data = await service.servers.findById(req.authUser, { id: serverId });
|
||||||
|
const success = ApiSuccess.ok({ data: toApiModel.servers([data]) });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
};
|
||||||
|
|
||||||
const { id } = req.params;
|
const getServerList = async (
|
||||||
const data = await serversService.findById(req.auth, {
|
req: TypedRequest<typeof validation.servers.list>,
|
||||||
id: Number(id),
|
res: Response
|
||||||
|
) => {
|
||||||
|
const data = await service.servers.findMany(req.authUser);
|
||||||
|
const success = ApiSuccess.ok({ data: toApiModel.servers(data) });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteServer = async (
|
||||||
|
req: TypedRequest<typeof validation.servers.deleteServer>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
await service.servers.deleteById({ id: serverId });
|
||||||
|
const success = ApiSuccess.noContent({ data: null });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createServer = async (
|
||||||
|
req: TypedRequest<typeof validation.servers.create>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const remoteServerLoginRes = await service.servers.remoteServerLogin(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await service.servers.create({
|
||||||
|
name: req.body.name,
|
||||||
|
...remoteServerLoginRes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getServers = async (req: Request, res: Response) => {
|
const updateServer = async (
|
||||||
const data = await serversService.findMany(req.auth);
|
req: TypedRequest<typeof validation.servers.update>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
const { username, password, name, legacy, type, url } = req.body;
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
if (type && username && password && url) {
|
||||||
|
const remoteServerLoginRes = await service.servers.remoteServerLogin({
|
||||||
|
legacy,
|
||||||
|
password,
|
||||||
|
type,
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await service.servers.update(
|
||||||
|
{ id: serverId },
|
||||||
|
{ name, ...remoteServerLoginRes }
|
||||||
|
);
|
||||||
|
|
||||||
|
const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await service.servers.update({ id: serverId }, { name, url });
|
||||||
|
const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const createServer = async (req: Request, res: Response) => {
|
const refreshServer = async (
|
||||||
const data = await serversService.create(req.body);
|
req: TypedRequest<typeof validation.servers.refresh>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
const data = await service.servers.refresh({ id: serverId });
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshServer = async (req: Request, res: Response) => {
|
const scanServer = async (
|
||||||
const { id } = req.params;
|
req: TypedRequest<typeof validation.servers.scan>,
|
||||||
const data = await serversService.refresh({ id: Number(id) });
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { serverId } = req.params;
|
||||||
|
const { serverFolderId } = req.body;
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
const data = await service.servers.fullScan({
|
||||||
};
|
id: serverId,
|
||||||
|
serverFolderId,
|
||||||
const scanServer = async (req: Request, res: Response) => {
|
|
||||||
validateRequest(req, {
|
|
||||||
query: z.object({ serverFolderIds: z.string().optional() }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { id } = req.params;
|
const success = ApiSuccess.ok({ data });
|
||||||
const { serverFolderIds } = req.query;
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
|
||||||
const data = await serversService.fullScan({
|
|
||||||
id: Number(id),
|
|
||||||
serverFolderIds: serverFolderIds && String(serverFolderIds),
|
|
||||||
userId: Number(req.auth.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFolder = async (req: Request, res: Response) => {
|
const createServerUrl = async (
|
||||||
const data = await prisma.folder.findUnique({
|
req: TypedRequest<typeof validation.servers.createUrl>,
|
||||||
include: {
|
res: Response
|
||||||
children: true,
|
) => {
|
||||||
},
|
const { serverId } = req.params;
|
||||||
where: { id: Number(req.params.id) },
|
const { url } = req.body;
|
||||||
|
|
||||||
|
const data = await service.servers.createUrl({
|
||||||
|
serverId,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json(getSuccessResponse({ data, statusCode: 200 }));
|
const success = ApiSuccess.ok({ data });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteServerUrl = async (
|
||||||
|
req: TypedRequest<typeof validation.servers.deleteUrl>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { urlId } = req.params;
|
||||||
|
|
||||||
|
await service.servers.deleteUrlById({
|
||||||
|
id: urlId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const success = ApiSuccess.noContent({ data: null });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableServerUrl = async (
|
||||||
|
req: TypedRequest<typeof validation.servers.enableUrl>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { serverId, urlId } = req.params;
|
||||||
|
|
||||||
|
await service.servers.enableUrlById(req.authUser, {
|
||||||
|
id: urlId,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const success = ApiSuccess.noContent({ data: null });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableServerUrl = async (
|
||||||
|
req: TypedRequest<typeof validation.servers.disableUrl>,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
await service.servers.disableUrlById(req.authUser);
|
||||||
|
|
||||||
|
const success = ApiSuccess.noContent({ data: null });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serversController = {
|
export const serversController = {
|
||||||
createServer,
|
createServer,
|
||||||
getFolder,
|
createServerUrl,
|
||||||
getServerById,
|
deleteServer,
|
||||||
getServers,
|
deleteServerUrl,
|
||||||
|
disableServerUrl,
|
||||||
|
enableServerUrl,
|
||||||
|
getServerDetail,
|
||||||
|
getServerList,
|
||||||
refreshServer,
|
refreshServer,
|
||||||
scanServer,
|
scanServer,
|
||||||
|
updateServer,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +1,33 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { z } from 'zod';
|
|
||||||
import { songsService } from '../services/songs.service';
|
|
||||||
import {
|
|
||||||
getSuccessResponse,
|
|
||||||
paginationValidation,
|
|
||||||
validateRequest,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
const getSongs = async (req: Request, res: Response) => {
|
const getSongList = async (req: Request, res: Response) => {
|
||||||
validateRequest(req, {
|
const { serverId } = req.params;
|
||||||
query: z.object({
|
const { take, skip, serverFolderId } = req.query;
|
||||||
...paginationValidation,
|
|
||||||
albumIds: z.optional(z.string()),
|
|
||||||
artistIds: z.optional(z.string()),
|
|
||||||
serverFolderIds: z.optional(z.string().min(1)),
|
|
||||||
songIds: z.optional(z.string()),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { take, skip, serverFolderIds } = req.query;
|
// const songs = await songsService.findMany(req, {
|
||||||
|
// serverFolderIds: String(serverFolderId),
|
||||||
|
// serverId,
|
||||||
|
// skip: Number(skip),
|
||||||
|
// take: Number(take),
|
||||||
|
// user: req.authUser,
|
||||||
|
// });
|
||||||
|
|
||||||
const data = await songsService.findMany(req, {
|
// const success = ApiSuccess.ok({
|
||||||
serverFolderIds: String(serverFolderIds),
|
// // data: toRes.songs(songs.data, req.authUser),
|
||||||
skip: Number(skip),
|
// data: songs.data,
|
||||||
take: Number(take),
|
// paginationItems: {
|
||||||
user: req.auth,
|
// skip: Number(skip),
|
||||||
});
|
// take: Number(take),
|
||||||
|
// totalEntries: songs.totalEntries,
|
||||||
|
// url: req.originalUrl,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
return {};
|
||||||
|
|
||||||
|
// return res.status(data.statusCode).json(getSuccessResponse(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const songsController = {
|
export const songsController = {
|
||||||
getSongs,
|
getSongList,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { z } from 'zod';
|
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||||
import { usersService } from '../services';
|
import { toApiModel } from '@helpers/api-model';
|
||||||
import { getSuccessResponse, idValidation, validateRequest } from '../utils';
|
import { service } from '@services/index';
|
||||||
|
|
||||||
const getUser = async (req: Request, res: Response) => {
|
const getUserDetail = async (req: Request, res: Response) => {
|
||||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const data = await usersService.getOne({ id: Number(id) });
|
const user = await service.users.findById(req.authUser, { id });
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsers = async (_req: Request, res: Response) => {
|
const getUserList = async (_req: Request, res: Response) => {
|
||||||
const data = await usersService.getMany();
|
const users = await service.users.findMany();
|
||||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
const success = ApiSuccess.ok({ data: toApiModel.users(users) });
|
||||||
|
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usersController = {
|
export const usersController = {
|
||||||
getUser,
|
getUserDetail,
|
||||||
getUsers,
|
getUserList,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,40 @@
|
|||||||
import { Prisma } from '@prisma/client';
|
import { AuthUser } from '@/middleware';
|
||||||
import { SortOrder } from '../types/types';
|
import { SortOrder } from '@/types/types';
|
||||||
import { splitNumberString } from '../utils';
|
import { songHelpers } from '@helpers/songs.helpers';
|
||||||
import { songHelpers } from './songs.helpers';
|
|
||||||
|
|
||||||
export enum AlbumSort {
|
export enum AlbumSort {
|
||||||
DATE_ADDED = 'date_added',
|
DATE_ADDED = 'added',
|
||||||
DATE_ADDED_REMOTE = 'date_added_remote',
|
DATE_ADDED_REMOTE = 'addedRemote',
|
||||||
DATE_RELEASED = 'date_released',
|
DATE_RELEASED = 'released',
|
||||||
|
DATE_RELEASED_YEAR = 'year',
|
||||||
|
FAVORITE = 'favorite',
|
||||||
|
NAME = 'name',
|
||||||
RANDOM = 'random',
|
RANDOM = 'random',
|
||||||
RATING = 'rating',
|
RATING = 'rating',
|
||||||
TITLE = 'title',
|
|
||||||
YEAR = 'year',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const include = (options?: { serverUrls?: string; songs: boolean }) => {
|
const include = (options: { songs?: boolean; user?: AuthUser }) => {
|
||||||
const props: Prisma.AlbumInclude = {
|
// Prisma.AlbumInclude
|
||||||
_count: { select: { favorites: true, songs: true } },
|
const props = {
|
||||||
albumArtist: true,
|
_count: {
|
||||||
genres: true,
|
select: {
|
||||||
images: true,
|
favorites: true,
|
||||||
ratings: true,
|
songs: true,
|
||||||
server: {
|
|
||||||
include: {
|
|
||||||
serverUrls: options?.serverUrls
|
|
||||||
? { where: { id: { in: splitNumberString(options.serverUrls) } } }
|
|
||||||
: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
albumArtists: true,
|
||||||
songs: options?.songs ? songHelpers.include() : false,
|
artists: true,
|
||||||
|
favorites: { where: { userId: options.user?.id } },
|
||||||
|
genres: true,
|
||||||
|
images: true,
|
||||||
|
ratings: {
|
||||||
|
where: {
|
||||||
|
userId: options.user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: true,
|
||||||
|
serverFolders: true,
|
||||||
|
songs: options?.songs && songHelpers.findMany(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return props;
|
return props;
|
||||||
@@ -38,7 +44,7 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => {
|
|||||||
let order;
|
let order;
|
||||||
|
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case AlbumSort.TITLE:
|
case AlbumSort.NAME:
|
||||||
order = { name: orderBy };
|
order = { name: orderBy };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -51,11 +57,19 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case AlbumSort.DATE_RELEASED:
|
case AlbumSort.DATE_RELEASED:
|
||||||
order = { date: orderBy, year: orderBy };
|
order = { releaseDate: orderBy, year: orderBy };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AlbumSort.YEAR:
|
case AlbumSort.DATE_RELEASED_YEAR:
|
||||||
order = { year: orderBy };
|
order = { releaseYear: orderBy };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AlbumSort.RATING:
|
||||||
|
order = { rating: orderBy };
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AlbumSort.FAVORITE:
|
||||||
|
order = { favorite: orderBy };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,520 @@
|
|||||||
|
/* eslint-disable no-underscore-dangle */
|
||||||
|
import {
|
||||||
|
Album,
|
||||||
|
AlbumArtist,
|
||||||
|
AlbumArtistRating,
|
||||||
|
AlbumRating,
|
||||||
|
Artist,
|
||||||
|
ArtistRating,
|
||||||
|
External,
|
||||||
|
Genre,
|
||||||
|
Image,
|
||||||
|
ImageType,
|
||||||
|
Server,
|
||||||
|
ServerFolder,
|
||||||
|
ServerFolderPermission,
|
||||||
|
ServerPermission,
|
||||||
|
ServerType,
|
||||||
|
ServerUrl,
|
||||||
|
Song,
|
||||||
|
SongRating,
|
||||||
|
User,
|
||||||
|
UserServerUrl,
|
||||||
|
} from '@prisma/client';
|
||||||
|
|
||||||
|
const getSubsonicStreamUrl = (
|
||||||
|
remoteId: string,
|
||||||
|
url: string,
|
||||||
|
token: string,
|
||||||
|
deviceId: string
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
`${url}/rest/stream.view` +
|
||||||
|
`?id=${remoteId}` +
|
||||||
|
`&${token}` +
|
||||||
|
`&v=1.13.0` +
|
||||||
|
`&c=sonixd_${deviceId}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getJellyfinStreamUrl = (
|
||||||
|
remoteId: string,
|
||||||
|
url: string,
|
||||||
|
token: string,
|
||||||
|
userId: string,
|
||||||
|
deviceId: string
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
`${url}/audio` +
|
||||||
|
`/${remoteId}/universal` +
|
||||||
|
`?userId=${userId}` +
|
||||||
|
`&audioCodec=aac` +
|
||||||
|
`&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg` +
|
||||||
|
`&transcodingContainer=ts` +
|
||||||
|
`&transcodingProtocol=hls` +
|
||||||
|
`&deviceId=sonixd_${deviceId}` +
|
||||||
|
`&playSessionId=${deviceId}` +
|
||||||
|
`&api_key=${token}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const streamUrl = (
|
||||||
|
type: ServerType,
|
||||||
|
args: {
|
||||||
|
deviceId: string;
|
||||||
|
remoteId: string;
|
||||||
|
token: string;
|
||||||
|
url: string;
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (type === ServerType.JELLYFIN) {
|
||||||
|
return getJellyfinStreamUrl(
|
||||||
|
args.remoteId,
|
||||||
|
args.url,
|
||||||
|
args.token,
|
||||||
|
args.userId || '',
|
||||||
|
args.deviceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return getSubsonicStreamUrl(
|
||||||
|
args.remoteId,
|
||||||
|
args.url,
|
||||||
|
args.token,
|
||||||
|
args.deviceId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageUrl = (
|
||||||
|
type: ServerType,
|
||||||
|
baseUrl: string,
|
||||||
|
imageId: string,
|
||||||
|
token?: string
|
||||||
|
) => {
|
||||||
|
if (type === ServerType.JELLYFIN) {
|
||||||
|
return (
|
||||||
|
`${baseUrl}/Items` +
|
||||||
|
`/${imageId}` +
|
||||||
|
`/Images/Primary` +
|
||||||
|
'?fillHeight=250' +
|
||||||
|
`&fillWidth=250` +
|
||||||
|
'&quality=90'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ServerType.SUBSONIC || type === ServerType.NAVIDROME) {
|
||||||
|
return (
|
||||||
|
`${baseUrl}/rest/getCoverArt.view` +
|
||||||
|
`?id=${imageId}` +
|
||||||
|
`&size=250` +
|
||||||
|
`&v=1.13.0` +
|
||||||
|
`&c=sonixd` +
|
||||||
|
`&${token}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedAlbum = (item: Album) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
remoteId: item.remoteId,
|
||||||
|
deleted: item.deleted,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedArtists = (items: Artist[]) => {
|
||||||
|
return (
|
||||||
|
items?.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
remoteId: item.remoteId,
|
||||||
|
deleted: item.deleted,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedAlbumArtists = (items: AlbumArtist[]) => {
|
||||||
|
return (
|
||||||
|
items?.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
remoteId: item.remoteId,
|
||||||
|
deleted: item.deleted,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedGenres = (items: Genre[]) => {
|
||||||
|
return (
|
||||||
|
items?.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedServerFolders = (items: ServerFolder[]) => {
|
||||||
|
const serverFolders = items?.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
remoteId: item.remoteId,
|
||||||
|
lastScannedAt: item.lastScannedAt,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return serverFolders || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedServerUrls = (
|
||||||
|
items: (ServerUrl & {
|
||||||
|
userServerUrls?: UserServerUrl[];
|
||||||
|
})[]
|
||||||
|
) => {
|
||||||
|
const serverUrls = items?.map((item) => {
|
||||||
|
const userServerUrlIds = item.userServerUrls?.map(
|
||||||
|
(userServerUrl) => userServerUrl.serverUrlId
|
||||||
|
);
|
||||||
|
const enabled = userServerUrlIds?.some((id) => id === item.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
url: item.url,
|
||||||
|
enabled,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return serverUrls || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const rating = (
|
||||||
|
items: AlbumRating[] | SongRating[] | ArtistRating[] | AlbumArtistRating[]
|
||||||
|
) => {
|
||||||
|
if (items.length > 0) {
|
||||||
|
return items[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const image = (
|
||||||
|
images: Image[],
|
||||||
|
type: ServerType,
|
||||||
|
imageType: ImageType,
|
||||||
|
url: string,
|
||||||
|
remoteId: string,
|
||||||
|
token?: string
|
||||||
|
) => {
|
||||||
|
const imageRemoteUrl = images.find((i) => i.type === imageType)?.remoteUrl;
|
||||||
|
|
||||||
|
if (!imageRemoteUrl) return null;
|
||||||
|
if (type === ServerType.JELLYFIN) {
|
||||||
|
return imageUrl(type, url, remoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ServerType.SUBSONIC || type === ServerType.NAVIDROME) {
|
||||||
|
return imageUrl(type, url, imageRemoteUrl, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DbSong = Song & DbSongInclude;
|
||||||
|
|
||||||
|
type DbSongInclude = {
|
||||||
|
album: Album;
|
||||||
|
artists: Artist[];
|
||||||
|
externals: External[];
|
||||||
|
genres: Genre[];
|
||||||
|
images: Image[];
|
||||||
|
ratings: SongRating[];
|
||||||
|
server: Server & { serverUrls: ServerUrl[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const songs = (
|
||||||
|
items: DbSong[],
|
||||||
|
options: {
|
||||||
|
deviceId: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
serverFolderId?: number;
|
||||||
|
token: string;
|
||||||
|
type: ServerType;
|
||||||
|
url: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
items?.map((item) => {
|
||||||
|
const url = options.url ? options.url : item.server.serverUrls[0].url;
|
||||||
|
|
||||||
|
const stream = streamUrl(options.type, {
|
||||||
|
deviceId: options.deviceId,
|
||||||
|
remoteId: item.remoteId,
|
||||||
|
token: options.token,
|
||||||
|
url: options.url,
|
||||||
|
userId: options.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
artistName: item.artistName,
|
||||||
|
album: item.album && relatedAlbum(item.album),
|
||||||
|
artists: relatedArtists(item.artists),
|
||||||
|
bitRate: item.bitRate,
|
||||||
|
container: item.container,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
deleted: item.deleted,
|
||||||
|
discNumber: item.discNumber,
|
||||||
|
duration: item.duration,
|
||||||
|
genres: relatedGenres(item.genres),
|
||||||
|
imageUrl: image(
|
||||||
|
item.images,
|
||||||
|
options.type,
|
||||||
|
ImageType.PRIMARY,
|
||||||
|
url,
|
||||||
|
item.remoteId
|
||||||
|
),
|
||||||
|
releaseDate: item.releaseDate,
|
||||||
|
releaseYear: item.releaseYear,
|
||||||
|
remoteCreatedAt: item.remoteCreatedAt,
|
||||||
|
remoteId: item.remoteId,
|
||||||
|
// serverFolderId: item.serverFolderId,
|
||||||
|
serverId: item.serverId,
|
||||||
|
streamUrl: stream,
|
||||||
|
trackNumber: item.trackNumber,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DbAlbum = Album & DbAlbumInclude;
|
||||||
|
|
||||||
|
type DbAlbumInclude = {
|
||||||
|
_count: {
|
||||||
|
favorites: number;
|
||||||
|
songs: number;
|
||||||
|
};
|
||||||
|
albumArtists: AlbumArtist[];
|
||||||
|
genres: Genre[];
|
||||||
|
images: Image[];
|
||||||
|
ratings: AlbumRating[];
|
||||||
|
server: Server;
|
||||||
|
serverFolders: ServerFolder[];
|
||||||
|
songs?: DbSong[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const albums = (options: {
|
||||||
|
items: DbAlbum[] | any[];
|
||||||
|
serverUrl?: string;
|
||||||
|
user: User;
|
||||||
|
}) => {
|
||||||
|
const { items, serverUrl, user } = options;
|
||||||
|
return (
|
||||||
|
items?.map((item) => {
|
||||||
|
const { type, token, remoteUserId } = item.server;
|
||||||
|
const url = serverUrl || item.server.url;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
sortName: item.sortName,
|
||||||
|
releaseDate: item.releaseDate,
|
||||||
|
releaseYear: item.releaseYear,
|
||||||
|
isFavorite: item.favorites.length === 1,
|
||||||
|
rating: rating(item.ratings),
|
||||||
|
songCount: item._count.songs,
|
||||||
|
type,
|
||||||
|
imageUrl: image(
|
||||||
|
item.images,
|
||||||
|
type,
|
||||||
|
ImageType.PRIMARY,
|
||||||
|
url,
|
||||||
|
item.remoteId
|
||||||
|
),
|
||||||
|
backdropImageUrl: image(
|
||||||
|
item.images,
|
||||||
|
type,
|
||||||
|
ImageType.BACKDROP,
|
||||||
|
url,
|
||||||
|
item.remoteId
|
||||||
|
),
|
||||||
|
deleted: item.deleted,
|
||||||
|
remoteId: item.remoteId,
|
||||||
|
remoteCreatedAt: item.remoteCreatedAt,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
genres: item.genres ? relatedGenres(item.genres) : [],
|
||||||
|
albumArtists: item.albumArtists
|
||||||
|
? relatedAlbumArtists(item.albumArtists)
|
||||||
|
: [],
|
||||||
|
artists: item.artists ? relatedArtists(item.artists) : [],
|
||||||
|
serverFolders: relatedServerFolders(item.serverFolders),
|
||||||
|
songs:
|
||||||
|
item.songs &&
|
||||||
|
songs(item.songs, {
|
||||||
|
deviceId: user.deviceId,
|
||||||
|
token,
|
||||||
|
type,
|
||||||
|
url,
|
||||||
|
userId: remoteUserId,
|
||||||
|
}),
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// const relatedServerCredentials = (items: ServerCredential[]) => {
|
||||||
|
// return (
|
||||||
|
// items.map((item) => {
|
||||||
|
// return {
|
||||||
|
// /* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
// id: item.id,
|
||||||
|
// enabled: item.enabled,
|
||||||
|
// username: item.username,
|
||||||
|
// credential: item.credential,
|
||||||
|
// /* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
// };
|
||||||
|
// }) || []
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const serverCredentials = (items: ServerCredential[]) => {
|
||||||
|
// return (
|
||||||
|
// items.map((item) => {
|
||||||
|
// return {
|
||||||
|
// /* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
// id: item.id,
|
||||||
|
// username: item.username,
|
||||||
|
// enabled: item.enabled,
|
||||||
|
// credential: item.credential,
|
||||||
|
// createdAt: item.createdAt,
|
||||||
|
// updatedAt: item.updatedAt,
|
||||||
|
// /* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
// };
|
||||||
|
// }) || []
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
const servers = (
|
||||||
|
items: (Server & {
|
||||||
|
serverFolders?: ServerFolder[];
|
||||||
|
serverUrls?: (ServerUrl & {
|
||||||
|
userServerUrls?: UserServerUrl[];
|
||||||
|
})[];
|
||||||
|
})[]
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
items.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
url: item.url,
|
||||||
|
type: item.type,
|
||||||
|
username: item.username,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
serverFolders:
|
||||||
|
item.serverFolders && relatedServerFolders(item.serverFolders),
|
||||||
|
serverUrls: item.serverUrls && relatedServerUrls(item.serverUrls),
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedServerFolderPermissions = (items: ServerFolderPermission[]) => {
|
||||||
|
return items.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
serverFolderId: item.serverFolderId,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const relatedServerPermissions = (items: ServerPermission[]) => {
|
||||||
|
return items.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
type: item.type,
|
||||||
|
serverId: item.serverId,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = (
|
||||||
|
items: (User & {
|
||||||
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
serverFolderPermissions?: ServerFolderPermission[];
|
||||||
|
serverPermissions?: ServerPermission[];
|
||||||
|
})[]
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
items.map((item) => {
|
||||||
|
return {
|
||||||
|
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
||||||
|
id: item.id,
|
||||||
|
username: item.username,
|
||||||
|
accessToken: item.accessToken,
|
||||||
|
refreshToken: item.refreshToken,
|
||||||
|
enabled: item.enabled,
|
||||||
|
isAdmin: item.isAdmin,
|
||||||
|
deviceId: item.deviceId,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
updatedAt: item.updatedAt,
|
||||||
|
flatServerPermissions:
|
||||||
|
item.serverPermissions && item.serverPermissions.map((s) => s.id),
|
||||||
|
serverFolderPermissions:
|
||||||
|
item.serverFolderPermissions &&
|
||||||
|
relatedServerFolderPermissions(item.serverFolderPermissions),
|
||||||
|
serverPermissions:
|
||||||
|
item.serverPermissions &&
|
||||||
|
relatedServerPermissions(item.serverPermissions),
|
||||||
|
/* eslint-enable sort-keys-fix/sort-keys-fix */
|
||||||
|
};
|
||||||
|
}) || []
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toApiModel = {
|
||||||
|
albums,
|
||||||
|
servers,
|
||||||
|
songs,
|
||||||
|
users,
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { albumHelpers } from './albums.helpers';
|
||||||
|
import { sharedHelpers } from './shared.helpers';
|
||||||
|
import { songHelpers } from './songs.helpers';
|
||||||
|
|
||||||
|
export const helpers = {
|
||||||
|
albums: albumHelpers,
|
||||||
|
shared: sharedHelpers,
|
||||||
|
songs: songHelpers,
|
||||||
|
};
|
||||||
@@ -1,11 +1,108 @@
|
|||||||
const serverFolderFilter = (serverFolderIds: number[]) => {
|
import { AuthUser } from '@/middleware';
|
||||||
return serverFolderIds!.map((serverFolderId: number) => {
|
import { ApiError } from '@/utils';
|
||||||
return {
|
import { prisma } from '@lib/prisma';
|
||||||
serverFolders: { some: { id: { equals: Number(serverFolderId) } } },
|
|
||||||
};
|
const checkServerPermissions = (
|
||||||
|
user: AuthUser,
|
||||||
|
options: { serverId?: string }
|
||||||
|
) => {
|
||||||
|
const { serverId } = options;
|
||||||
|
|
||||||
|
if (user.isAdmin || !serverId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverId && !user.flatServerPermissions.includes(serverId)) {
|
||||||
|
throw ApiError.forbidden();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkServerFolderPermissions = (
|
||||||
|
user: AuthUser,
|
||||||
|
options: { serverFolderId?: string[] | string }
|
||||||
|
) => {
|
||||||
|
const { serverFolderId } = options;
|
||||||
|
|
||||||
|
if (user.isAdmin || !serverFolderId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ids: string[] = [];
|
||||||
|
if (typeof serverFolderId === 'string') {
|
||||||
|
ids = [serverFolderId];
|
||||||
|
} else if (typeof serverFolderId === 'object') {
|
||||||
|
ids = serverFolderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
if (!user.flatServerFolderPermissions.includes(id)) {
|
||||||
|
throw ApiError.forbidden('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvailableServerFolderIds = async (
|
||||||
|
user: AuthUser,
|
||||||
|
options: { serverId: string }
|
||||||
|
) => {
|
||||||
|
const { serverId } = options;
|
||||||
|
|
||||||
|
if (user.isAdmin) {
|
||||||
|
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||||
|
where: { serverId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
|
||||||
|
(serverFolder) => serverFolder.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return serverFoldersWithAccessIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
serverFolderPermissions: {
|
||||||
|
some: { userId: { equals: user.id } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
|
||||||
|
(serverFolder) => serverFolder.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return serverFoldersWithAccessIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serverFolderFilter = (serverFolderIds: string[]) => {
|
||||||
|
return {
|
||||||
|
serverFolders: { every: { id: { in: serverFolderIds } } },
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const paginationParams = (options: { skip: any; take: any }) => {
|
||||||
|
const { skip, take } = options;
|
||||||
|
|
||||||
|
return {
|
||||||
|
skip: Number(skip),
|
||||||
|
take: Number(take),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sharedHelpers = {
|
export const sharedHelpers = {
|
||||||
|
checkServerFolderPermissions,
|
||||||
|
checkServerPermissions,
|
||||||
|
getAvailableServerFolderIds,
|
||||||
|
params: {
|
||||||
|
pagination: paginationParams,
|
||||||
|
},
|
||||||
serverFolderFilter,
|
serverFolderFilter,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,20 +1,45 @@
|
|||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
const include = () => {
|
const include = () => {
|
||||||
const body = {
|
const props: Prisma.SongInclude = {
|
||||||
|
album: true,
|
||||||
|
artists: true,
|
||||||
|
externals: true,
|
||||||
|
genres: true,
|
||||||
|
images: true,
|
||||||
|
ratings: true,
|
||||||
|
server: {
|
||||||
|
include: { serverUrls: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findMany = () => {
|
||||||
|
const props: Prisma.SongFindManyArgs = {
|
||||||
include: {
|
include: {
|
||||||
album: true,
|
album: true,
|
||||||
artists: true,
|
artists: true,
|
||||||
externals: true,
|
externals: true,
|
||||||
genres: true,
|
genres: true,
|
||||||
images: true,
|
images: true,
|
||||||
|
ratings: true,
|
||||||
|
server: {
|
||||||
|
include: { serverUrls: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: [{ disc: Prisma.SortOrder.asc }, { track: Prisma.SortOrder.asc }],
|
orderBy: [
|
||||||
|
// { albumId: Prisma.SortOrder.asc },
|
||||||
|
{ discNumber: Prisma.SortOrder.asc },
|
||||||
|
{ trackNumber: Prisma.SortOrder.asc },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return body;
|
return props;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const songHelpers = {
|
export const songHelpers = {
|
||||||
|
findMany,
|
||||||
include,
|
include,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { ServerFolderPermission, ServerPermission, User } from '@prisma/client';
|
||||||
ServerCredential,
|
|
||||||
ServerFolderPermission,
|
|
||||||
ServerPermission,
|
|
||||||
User,
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
|
|
||||||
@@ -55,13 +50,6 @@ export const authenticate = (
|
|||||||
(permission: ServerPermission) => permission.serverId
|
(permission: ServerPermission) => permission.serverId
|
||||||
);
|
);
|
||||||
|
|
||||||
const serverCredentials = user.serverCredentials.map(
|
|
||||||
(credential: ServerCredential) => ({
|
|
||||||
id: credential.id,
|
|
||||||
serverId: credential.serverId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
createdAt: user?.createdAt,
|
createdAt: user?.createdAt,
|
||||||
enabled: user?.enabled,
|
enabled: user?.enabled,
|
||||||
@@ -70,7 +58,6 @@ export const authenticate = (
|
|||||||
id: user?.id,
|
id: user?.id,
|
||||||
isAdmin: user?.isAdmin,
|
isAdmin: user?.isAdmin,
|
||||||
server: req.params.serverId,
|
server: req.params.serverId,
|
||||||
serverCredentials,
|
|
||||||
serverFolderPermissions: user?.serverFolderPermissions,
|
serverFolderPermissions: user?.serverFolderPermissions,
|
||||||
serverPermissions: user?.serverPermissions,
|
serverPermissions: user?.serverPermissions,
|
||||||
updatedAt: user?.updatedAt,
|
updatedAt: user?.updatedAt,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
import { isJsonString } from '../utils';
|
import { isJsonString } from '@utils/is-json-string';
|
||||||
|
|
||||||
export const errorHandler = (
|
export const errorHandler = (
|
||||||
err: any,
|
err: any,
|
||||||
@@ -9,7 +9,7 @@ export const errorHandler = (
|
|||||||
) => {
|
) => {
|
||||||
let message = '';
|
let message = '';
|
||||||
|
|
||||||
const trace = err.stack.match(/at .* \(.*\)/g).map((e: string) => {
|
const trace = err.stack?.match(/at .* \(.*\)/g).map((e: string) => {
|
||||||
return e.replace(/\(|\)/g, '');
|
return e.replace(/\(|\)/g, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import { controller } from '../controllers';
|
import { controller } from '@controllers/index';
|
||||||
|
import { validation, validateRequest } from '@validations/index';
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', controller.albumArtists.getAlbumArtists);
|
router.get('/', controller.albumArtists.getList);
|
||||||
|
|
||||||
router.get('/:id', controller.albumArtists.getAlbumArtistById);
|
router.get(
|
||||||
|
':serverId',
|
||||||
|
validateRequest(validation.albumArtists.detail),
|
||||||
|
controller.albumArtists.getDetail
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,8 +1,23 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import { controller } from '../controllers';
|
import { controller } from '@controllers/index';
|
||||||
|
import { validateRequest, validation } from '@validations/index';
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', controller.albums.getAlbumList);
|
router.get(
|
||||||
|
'/',
|
||||||
|
validateRequest(validation.albums.list),
|
||||||
|
controller.albums.getList
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/:id', controller.albums.getAlbumDetail);
|
router.get(
|
||||||
|
'/:albumId',
|
||||||
|
validateRequest(validation.albums.detail),
|
||||||
|
controller.albums.getDetail
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:albumId/songs',
|
||||||
|
validateRequest(validation.albums.detail),
|
||||||
|
controller.albums.getDetailSongList
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import { controller } from '../controllers';
|
import { controller } from '@controllers/index';
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', controller.artists.getArtists);
|
router.get('/', controller.artists.getList);
|
||||||
|
|
||||||
router.get('/:id', controller.artists.getArtistById);
|
router.get(':serverId', controller.artists.getDetail);
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import { controller } from '../controllers';
|
import { controller } from '@controllers/index';
|
||||||
import { authenticate } from '../middleware';
|
import { authenticate } from '@middleware/authenticate';
|
||||||
|
import { validation, validateRequest } from '@validations/index';
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.post('/login', passport.authenticate('local'), controller.auth.login);
|
router.post(
|
||||||
|
'/login',
|
||||||
|
validateRequest(validation.auth.login),
|
||||||
|
passport.authenticate('local'),
|
||||||
|
controller.auth.login
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/register', controller.auth.register);
|
router.post(
|
||||||
|
'/register',
|
||||||
|
validateRequest(validation.auth.register),
|
||||||
|
controller.auth.register
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/logout', authenticate, controller.auth.logout);
|
router.post('/logout', authenticate, controller.auth.logout);
|
||||||
|
|
||||||
router.post('/refresh', controller.auth.refresh);
|
router.post(
|
||||||
|
'/refresh',
|
||||||
|
validateRequest(validation.auth.refresh),
|
||||||
|
controller.auth.refresh
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/ping', controller.auth.ping);
|
router.get('/ping', controller.auth.ping);
|
||||||
|
|||||||
@@ -23,7 +23,21 @@ routes.use('/api/users', usersRouter);
|
|||||||
routes.use('/api/servers', serversRouter);
|
routes.use('/api/servers', serversRouter);
|
||||||
|
|
||||||
routes.param('serverId', (req, _res, next, serverId) => {
|
routes.param('serverId', (req, _res, next, serverId) => {
|
||||||
helpers.shared.checkServerPermissions(req.auth, { serverId });
|
const { serverFolderId } = req.query as {
|
||||||
|
serverFolderId?: string[] | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
req.authUser.serverId = serverId;
|
||||||
|
|
||||||
|
helpers.shared.checkServerPermissions(req.authUser, { serverId });
|
||||||
|
helpers.shared.checkServerFolderPermissions(req.authUser, {
|
||||||
|
serverFolderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof req.query.serverFolderId === 'string') {
|
||||||
|
req.query.serverFolderId = [req.query.serverFolderId];
|
||||||
|
}
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,87 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import { controller } from '../controllers';
|
import { controller } from '@controllers/index';
|
||||||
import { authenticateAdmin } from '../middleware';
|
import { authenticateAdmin } from '@middleware/authenticate-admin';
|
||||||
|
import { service } from '@services/index';
|
||||||
|
import { validateRequest, validation } from '@validations/index';
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', controller.servers.getServerList);
|
router
|
||||||
|
.route('/')
|
||||||
|
.get(
|
||||||
|
validateRequest(validation.servers.list),
|
||||||
|
controller.servers.getServerList
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
authenticateAdmin,
|
||||||
|
validateRequest(validation.servers.create),
|
||||||
|
controller.servers.createServer
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/', authenticateAdmin, controller.servers.createServer);
|
router
|
||||||
|
.route('/:serverId')
|
||||||
|
.get(
|
||||||
|
validateRequest(validation.servers.detail),
|
||||||
|
controller.servers.getServerDetail
|
||||||
|
)
|
||||||
|
.patch(
|
||||||
|
authenticateAdmin,
|
||||||
|
validateRequest(validation.servers.update),
|
||||||
|
controller.servers.updateServer
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
authenticateAdmin,
|
||||||
|
validateRequest(validation.servers.deleteServer),
|
||||||
|
controller.servers.deleteServer
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/:id', controller.servers.getServerDetail);
|
router
|
||||||
|
.route('/:serverId/refresh')
|
||||||
|
.get(
|
||||||
|
authenticateAdmin,
|
||||||
|
validateRequest(validation.servers.refresh),
|
||||||
|
controller.servers.refreshServer
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/:id/refresh', authenticateAdmin, controller.servers.refreshServer);
|
router
|
||||||
|
.route('/:serverId/scan')
|
||||||
|
.post(
|
||||||
|
validateRequest(validation.servers.scan),
|
||||||
|
authenticateAdmin,
|
||||||
|
controller.servers.scanServer
|
||||||
|
);
|
||||||
|
|
||||||
router.get('/:id/folder', authenticateAdmin, controller.servers.getFolder);
|
router
|
||||||
|
.route('/:serverId/url')
|
||||||
|
.post(
|
||||||
|
authenticateAdmin,
|
||||||
|
validateRequest(validation.servers.createUrl),
|
||||||
|
controller.servers.createServerUrl
|
||||||
|
);
|
||||||
|
|
||||||
router.post('/:id/scan', authenticateAdmin, controller.servers.scanServer);
|
router.param('urlId', async (_req, _res, next, urlId) => {
|
||||||
|
await service.servers.findUrlById({ id: urlId });
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router
|
||||||
|
.route('/:serverId/url/:urlId')
|
||||||
|
.delete(
|
||||||
|
authenticateAdmin,
|
||||||
|
validateRequest(validation.servers.deleteUrl),
|
||||||
|
controller.servers.deleteServerUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
router
|
||||||
|
.route('/:serverId/url/:urlId/enable')
|
||||||
|
.post(
|
||||||
|
validateRequest(validation.servers.enableUrl),
|
||||||
|
controller.servers.enableServerUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
router
|
||||||
|
.route('/:serverId/url/:urlId/disable')
|
||||||
|
.post(
|
||||||
|
validateRequest(validation.servers.disableUrl),
|
||||||
|
controller.servers.disableServerUrl
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import { controller } from '../controllers';
|
import { validation, validateRequest } from '@validations/index';
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', controller.songs.getSongs);
|
router.get('/', validateRequest(validation.songs.list), async (req, res) => {
|
||||||
|
// const data = await controller.songs.getSongList(req.authUser, req.query);
|
||||||
|
|
||||||
|
return res.status(200).json({});
|
||||||
|
// return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import express, { Router } from 'express';
|
import express, { Router } from 'express';
|
||||||
import { controller } from '../controllers';
|
import { controller } from '@controllers/index';
|
||||||
|
import { validateRequest, validation } from '@validations/index';
|
||||||
|
import { authenticateAdmin } from '../middleware/authenticate-admin';
|
||||||
|
|
||||||
export const router: Router = express.Router({ mergeParams: true });
|
export const router: Router = express.Router({ mergeParams: true });
|
||||||
|
|
||||||
router.get('/', controller.users.getUsers);
|
router.get('/', authenticateAdmin, controller.users.getUserList);
|
||||||
|
|
||||||
router.get('/:id', controller.users.getUser);
|
router.get(
|
||||||
|
':serverId',
|
||||||
|
validateRequest(validation.users.detail),
|
||||||
|
controller.users.getUserDetail
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import cors from 'cors';
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import 'express-async-errors';
|
import 'express-async-errors';
|
||||||
import { errorHandler } from './middleware';
|
import { errorHandler } from '@/middleware';
|
||||||
import { routes } from './routes';
|
import { routes } from '@routes/index';
|
||||||
|
|
||||||
require('./lib/passport');
|
require('./lib/passport');
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
|
import { User } from '@prisma/client';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { prisma } from '../lib';
|
import { OffsetPagination } from '@/types/types';
|
||||||
import { OffsetPagination, User } from '../types/types';
|
import { ApiError } from '@/utils';
|
||||||
import {
|
import { prisma } from '@lib/prisma';
|
||||||
ApiError,
|
import { folderPermissions } from '@utils/folder-permissions';
|
||||||
ApiSuccess,
|
|
||||||
folderPermissions,
|
|
||||||
splitNumberString,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
const findById = async (options: { id: number; user: User }) => {
|
const findById = async (options: { id: string; user: User }) => {
|
||||||
const { id, user } = options;
|
const { id, user } = options;
|
||||||
const albumArtist = await prisma.albumArtist.findUnique({
|
const albumArtist = await prisma.albumArtist.findUnique({
|
||||||
include: {
|
include: {
|
||||||
@@ -32,7 +29,7 @@ const findById = async (options: { id: number; user: User }) => {
|
|||||||
throw ApiError.forbidden('');
|
throw ApiError.forbidden('');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: albumArtist });
|
return albumArtist;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findMany = async (
|
const findMany = async (
|
||||||
@@ -40,37 +37,29 @@ const findMany = async (
|
|||||||
options: { serverFolderIds: string; user: User } & OffsetPagination
|
options: { serverFolderIds: string; user: User } & OffsetPagination
|
||||||
) => {
|
) => {
|
||||||
const { user, take, serverFolderIds: rServerFolderIds, skip } = options;
|
const { user, take, serverFolderIds: rServerFolderIds, skip } = options;
|
||||||
const serverFolderIds = splitNumberString(rServerFolderIds);
|
const serverFolderIds = rServerFolderIds.split(',');
|
||||||
|
|
||||||
if (!(await folderPermissions(serverFolderIds!, user))) {
|
if (!(await folderPermissions(serverFolderIds!, user))) {
|
||||||
throw ApiError.forbidden('');
|
throw ApiError.forbidden('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
|
||||||
return {
|
serverFolders: { some: { id: { equals: serverFolderId } } },
|
||||||
serverFolders: { some: { id: { equals: Number(serverFolderId) } } },
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalEntries = await prisma.albumArtist.count({
|
const [totalEntries, albumArtists] = await prisma.$transaction([
|
||||||
where: { OR: serverFoldersFilter },
|
prisma.albumArtist.count({
|
||||||
});
|
where: { OR: serverFoldersFilter },
|
||||||
const albumArtists = await prisma.albumArtist.findMany({
|
}),
|
||||||
include: { genres: true },
|
prisma.albumArtist.findMany({
|
||||||
skip,
|
include: { genres: true },
|
||||||
take,
|
|
||||||
where: { OR: serverFoldersFilter },
|
|
||||||
});
|
|
||||||
|
|
||||||
return ApiSuccess.ok({
|
|
||||||
data: albumArtists,
|
|
||||||
paginationItems: {
|
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
totalEntries,
|
where: { OR: serverFoldersFilter },
|
||||||
url: req.originalUrl,
|
}),
|
||||||
},
|
]);
|
||||||
});
|
|
||||||
|
return { data: albumArtists, totalEntries };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const albumArtistsService = {
|
export const albumArtistsService = {
|
||||||
|
|||||||
@@ -1,30 +1,15 @@
|
|||||||
import { Album } from '@prisma/client';
|
import { AuthUser } from '@/middleware';
|
||||||
import { Request } from 'express';
|
import { OffsetPagination, SortOrder } from '@/types/types';
|
||||||
import { albumHelpers, AlbumSort } from '../helpers/albums.helpers';
|
import { ApiError } from '@/utils';
|
||||||
import { sharedHelpers } from '../helpers/shared.helpers';
|
import { AlbumSort } from '@helpers/albums.helpers';
|
||||||
import { prisma } from '../lib';
|
import { helpers } from '@helpers/index';
|
||||||
import { OffsetPagination, SortOrder, User } from '../types/types';
|
import { prisma } from '@lib/prisma';
|
||||||
import {
|
|
||||||
ApiError,
|
|
||||||
ApiSuccess,
|
|
||||||
folderPermissions,
|
|
||||||
getFolderPermissions,
|
|
||||||
splitNumberString,
|
|
||||||
} from '../utils';
|
|
||||||
import { toRes } from './response';
|
|
||||||
|
|
||||||
const findById = async (options: {
|
const findById = async (user: AuthUser, options: { id: string }) => {
|
||||||
id: number;
|
const { id } = options;
|
||||||
serverUrls?: string;
|
|
||||||
user: User;
|
|
||||||
}) => {
|
|
||||||
const { id, user, serverUrls } = options;
|
|
||||||
|
|
||||||
const album = await prisma.album.findUnique({
|
const album = await prisma.album.findUnique({
|
||||||
include: {
|
include: helpers.albums.include({ songs: true }),
|
||||||
...albumHelpers.include({ serverUrls, songs: true }),
|
|
||||||
serverFolders: true,
|
|
||||||
},
|
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,105 +17,94 @@ const findById = async (options: {
|
|||||||
throw ApiError.notFound('');
|
throw ApiError.notFound('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverFolderIds = album.serverFolders.map(
|
const serverFolderId = album.serverFolders.map((s) => s.id);
|
||||||
(serverFolder) => serverFolder.id
|
helpers.shared.checkServerFolderPermissions(user, { serverFolderId });
|
||||||
);
|
|
||||||
|
|
||||||
if (!(await folderPermissions(serverFolderIds, user))) {
|
return album;
|
||||||
throw ApiError.forbidden('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: toRes.albums([album], user)[0] });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const findMany = async (
|
export type AlbumFindManyOptions = {
|
||||||
req: Request,
|
orderBy: SortOrder;
|
||||||
options: {
|
serverFolderId?: string[];
|
||||||
orderBy: SortOrder;
|
serverId: string;
|
||||||
serverFolderIds?: string;
|
sortBy: AlbumSort;
|
||||||
serverUrls?: string;
|
user: AuthUser;
|
||||||
sortBy: AlbumSort;
|
} & OffsetPagination;
|
||||||
user: User;
|
|
||||||
} & OffsetPagination
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
user,
|
|
||||||
take,
|
|
||||||
serverFolderIds: rServerFolderIds,
|
|
||||||
serverUrls,
|
|
||||||
skip,
|
|
||||||
sortBy,
|
|
||||||
orderBy,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const serverFolderIds = rServerFolderIds
|
const findMany = async (options: AlbumFindManyOptions) => {
|
||||||
? splitNumberString(rServerFolderIds)
|
const { take, serverFolderId, skip, sortBy, orderBy, user, serverId } =
|
||||||
: await getFolderPermissions(user);
|
options;
|
||||||
|
|
||||||
if (!(await folderPermissions(serverFolderIds!, user))) {
|
const serverFolderIds =
|
||||||
throw ApiError.forbidden('');
|
serverFolderId ||
|
||||||
}
|
(await helpers.shared.getAvailableServerFolderIds(user, { serverId }));
|
||||||
|
|
||||||
const serverFoldersFilter = sharedHelpers.serverFolderFilter(
|
|
||||||
serverFolderIds!
|
|
||||||
);
|
|
||||||
|
|
||||||
let totalEntries = 0;
|
let totalEntries = 0;
|
||||||
let albums: Album[];
|
let albums;
|
||||||
|
|
||||||
if (sortBy === AlbumSort.RATING) {
|
if (sortBy === AlbumSort.RATING) {
|
||||||
const [count, result] = await prisma.$transaction([
|
const [count, result] = await prisma.$transaction([
|
||||||
prisma.albumRating.count({
|
prisma.albumRating.count({
|
||||||
where: {
|
where: {
|
||||||
album: { OR: serverFoldersFilter },
|
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||||
user: { id: user.id },
|
user: { id: user.id },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.albumRating.findMany({
|
prisma.albumRating.findMany({
|
||||||
include: {
|
include: {
|
||||||
album: {
|
album: {
|
||||||
include: { ...albumHelpers.include({ serverUrls, songs: false }) },
|
include: helpers.albums.include({ songs: false, user }),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: { value: orderBy },
|
orderBy: { value: orderBy },
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where: {
|
where: {
|
||||||
album: { OR: serverFoldersFilter },
|
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||||
user: { id: user.id },
|
user: { id: user.id },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
albums = result.map((rating) => rating.album);
|
||||||
albums = result.map((rating) => rating.album) as Album[];
|
|
||||||
totalEntries = count;
|
totalEntries = count;
|
||||||
} else {
|
} else if (sortBy === AlbumSort.FAVORITE) {
|
||||||
const [count, result] = await prisma.$transaction([
|
[totalEntries, albums] = await prisma.$transaction([
|
||||||
prisma.album.count({
|
prisma.album.count({
|
||||||
where: { OR: serverFoldersFilter },
|
where: {
|
||||||
|
AND: [
|
||||||
|
helpers.shared.serverFolderFilter(serverFolderIds),
|
||||||
|
{ favorites: { some: { userId: user.id } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
prisma.album.findMany({
|
prisma.album.findMany({
|
||||||
include: { ...albumHelpers.include({ serverUrls, songs: false }) },
|
include: helpers.albums.include({ songs: false, user }),
|
||||||
orderBy: [{ ...albumHelpers.sort(sortBy, orderBy) }],
|
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where: { OR: serverFoldersFilter },
|
where: {
|
||||||
|
AND: [
|
||||||
|
helpers.shared.serverFolderFilter(serverFolderIds),
|
||||||
|
{ favorites: { some: { userId: user.id } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
[totalEntries, albums] = await prisma.$transaction([
|
||||||
|
prisma.album.count({
|
||||||
|
where: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||||
|
}),
|
||||||
|
prisma.album.findMany({
|
||||||
|
include: helpers.albums.include({ songs: false, user }),
|
||||||
|
orderBy: [helpers.albums.sort(sortBy, orderBy)],
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
where: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
albums = result;
|
|
||||||
totalEntries = count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({
|
return { data: albums, totalEntries };
|
||||||
data: toRes.albums(albums, user),
|
|
||||||
paginationItems: {
|
|
||||||
skip,
|
|
||||||
take,
|
|
||||||
totalEntries,
|
|
||||||
url: req.originalUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const albumsService = {
|
export const albumsService = {
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
|
import { User } from '@prisma/client';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { prisma } from '../lib';
|
import { prisma } from '../lib';
|
||||||
import { OffsetPagination, User } from '../types/types';
|
import { OffsetPagination } from '../types/types';
|
||||||
import {
|
import { ApiError, folderPermissions } from '../utils';
|
||||||
ApiError,
|
|
||||||
ApiSuccess,
|
|
||||||
folderPermissions,
|
|
||||||
splitNumberString,
|
|
||||||
} from '../utils';
|
|
||||||
|
|
||||||
const findById = async (options: { id: number; user: User }) => {
|
const findById = async (options: { id: string; user: User }) => {
|
||||||
const { id, user } = options;
|
const { id, user } = options;
|
||||||
|
|
||||||
const artist = await prisma.artist.findUnique({
|
const artist = await prisma.artist.findUnique({
|
||||||
@@ -16,19 +12,16 @@ const findById = async (options: { id: number; user: User }) => {
|
|||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!artist) {
|
if (!artist) throw ApiError.notFound('');
|
||||||
throw ApiError.notFound('');
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverFolderIds = artist.serverFolders.map(
|
const serverFolderIds = artist.serverFolders.map(
|
||||||
(serverFolder) => serverFolder.id
|
(serverFolder) => serverFolder.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!(await folderPermissions(serverFolderIds, user))) {
|
if (!(await folderPermissions(serverFolderIds, user)))
|
||||||
throw ApiError.forbidden('');
|
throw ApiError.forbidden('');
|
||||||
}
|
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: artist });
|
return artist;
|
||||||
};
|
};
|
||||||
|
|
||||||
const findMany = async (
|
const findMany = async (
|
||||||
@@ -36,41 +29,28 @@ const findMany = async (
|
|||||||
options: { serverFolderIds: string; user: User } & OffsetPagination
|
options: { serverFolderIds: string; user: User } & OffsetPagination
|
||||||
) => {
|
) => {
|
||||||
const { user, skip, take, serverFolderIds: rServerFolderIds } = options;
|
const { user, skip, take, serverFolderIds: rServerFolderIds } = options;
|
||||||
const serverFolderIds = splitNumberString(rServerFolderIds);
|
const serverFolderIds = rServerFolderIds.split(',');
|
||||||
|
|
||||||
if (!(await folderPermissions(serverFolderIds!, user))) {
|
if (!(await folderPermissions(serverFolderIds!, user)))
|
||||||
throw ApiError.forbidden('');
|
throw ApiError.forbidden('');
|
||||||
}
|
|
||||||
|
|
||||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
|
||||||
return {
|
serverFolders: { some: { id: { equals: serverFolderId } } },
|
||||||
serverFolders: {
|
}));
|
||||||
some: {
|
|
||||||
id: { equals: Number(serverFolderId) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalEntries = await prisma.artist.count({
|
const [totalEntries, artists] = await prisma.$transaction([
|
||||||
where: { OR: serverFoldersFilter },
|
prisma.artist.count({
|
||||||
});
|
where: { OR: serverFoldersFilter },
|
||||||
const artists = await prisma.artist.findMany({
|
}),
|
||||||
include: { genres: true },
|
prisma.artist.findMany({
|
||||||
skip,
|
include: { genres: true },
|
||||||
take,
|
|
||||||
where: { OR: serverFoldersFilter },
|
|
||||||
});
|
|
||||||
|
|
||||||
return ApiSuccess.ok({
|
|
||||||
data: artists,
|
|
||||||
paginationItems: {
|
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
totalEntries,
|
where: { OR: serverFoldersFilter },
|
||||||
url: req.originalUrl,
|
}),
|
||||||
},
|
]);
|
||||||
});
|
|
||||||
|
return { data: artists, totalEntries };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const artistsService = {
|
export const artistsService = {
|
||||||
|
|||||||
@@ -8,22 +8,32 @@ import { ApiError } from '../utils/api-error';
|
|||||||
|
|
||||||
const login = async (options: { username: string }) => {
|
const login = async (options: { username: string }) => {
|
||||||
const { username } = options;
|
const { username } = options;
|
||||||
const user = await prisma.user.findUnique({ where: { username } });
|
const user = await prisma.user.findUnique({
|
||||||
|
include: { serverFolderPermissions: true, serverPermissions: true },
|
||||||
|
where: { username },
|
||||||
|
});
|
||||||
|
|
||||||
if (user) {
|
if (!user) {
|
||||||
const accessToken = generateToken(user.id);
|
throw ApiError.notFound('The user does not exist.');
|
||||||
const refreshToken = generateRefreshToken(user.id);
|
|
||||||
|
|
||||||
await prisma.refreshToken.create({
|
|
||||||
data: { token: refreshToken, userId: user.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const res = { ...user, accessToken, refreshToken };
|
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: res });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw ApiError.notFound('The user does not exist.');
|
const serverPermissions = user.serverPermissions.map((p) => p.id);
|
||||||
|
|
||||||
|
const otherProperties = {
|
||||||
|
isAdmin: user.isAdmin,
|
||||||
|
serverFolderPermissions: user.serverFolderPermissions.map((p) => p.id),
|
||||||
|
serverPermissions,
|
||||||
|
username: user.username,
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessToken = generateToken(user.id, otherProperties);
|
||||||
|
const refreshToken = generateRefreshToken(user.id, otherProperties);
|
||||||
|
|
||||||
|
await prisma.refreshToken.create({
|
||||||
|
data: { token: refreshToken, userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...user, accessToken, refreshToken };
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = async (options: { password: string; username: string }) => {
|
const register = async (options: { password: string; username: string }) => {
|
||||||
@@ -44,7 +54,7 @@ const register = async (options: { password: string; username: string }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: user });
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async (options: { user: User }) => {
|
const logout = async (options: { user: User }) => {
|
||||||
@@ -59,19 +69,16 @@ const logout = async (options: { user: User }) => {
|
|||||||
const refresh = async (options: { refreshToken: string }) => {
|
const refresh = async (options: { refreshToken: string }) => {
|
||||||
const { refreshToken } = options;
|
const { refreshToken } = options;
|
||||||
const user = jwt.verify(refreshToken, String(process.env.TOKEN_SECRET));
|
const user = jwt.verify(refreshToken, String(process.env.TOKEN_SECRET));
|
||||||
const { id } = user as { exp: number; iat: number; id: number };
|
const { id } = user as { exp: number; iat: number; id: string };
|
||||||
|
|
||||||
const token = await prisma.refreshToken.findUnique({
|
const token = await prisma.refreshToken.findUnique({
|
||||||
where: { token: refreshToken },
|
where: { token: refreshToken },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!token) {
|
if (!token) throw ApiError.unauthorized('Invalid refresh token.');
|
||||||
throw ApiError.unauthorized('Invalid refresh token.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newToken = generateToken(id);
|
const newToken = generateToken(id);
|
||||||
|
return { accessToken: newToken };
|
||||||
return ApiSuccess.ok({ data: { accessToken: newToken } });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authService = {
|
export const authService = {
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
export * from './auth.service';
|
import { albumArtistsService } from './album-artists.service';
|
||||||
export * from './servers.service';
|
import { albumsService } from './albums.service';
|
||||||
export * from './album-artists.service';
|
import { artistsService } from './artists.service';
|
||||||
export * from './users.service';
|
import { authService } from './auth.service';
|
||||||
export * from './artists.service';
|
import { serversService } from './servers.service';
|
||||||
export * from './albums.service';
|
import { usersService } from './users.service';
|
||||||
|
|
||||||
|
export const service = {
|
||||||
|
albumArtists: albumArtistsService,
|
||||||
|
albums: albumsService,
|
||||||
|
artists: artistsService,
|
||||||
|
auth: authService,
|
||||||
|
servers: serversService,
|
||||||
|
users: usersService,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,240 +0,0 @@
|
|||||||
/* eslint-disable no-underscore-dangle */
|
|
||||||
import meanBy from 'lodash/meanBy';
|
|
||||||
import { Item, Rating, User } from '../types/types';
|
|
||||||
import { getImageUrl } from '../utils';
|
|
||||||
|
|
||||||
const getSubsonicStreamUrl = (
|
|
||||||
remoteId: string,
|
|
||||||
url: string,
|
|
||||||
token: string,
|
|
||||||
deviceId: string
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
`${url}/rest/stream.view` +
|
|
||||||
`?id=${remoteId}` +
|
|
||||||
`&${token}` +
|
|
||||||
`&v=1.13.0` +
|
|
||||||
`&c=sonixd_${deviceId}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getJellyfinStreamUrl = (
|
|
||||||
remoteId: string,
|
|
||||||
url: string,
|
|
||||||
token: string,
|
|
||||||
userId: string,
|
|
||||||
deviceId: string
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
`${url}/audio` +
|
|
||||||
`/${remoteId}/universal` +
|
|
||||||
`?userId=${userId}` +
|
|
||||||
`&audioCodec=aac` +
|
|
||||||
`&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg` +
|
|
||||||
`&transcodingContainer=ts` +
|
|
||||||
`&transcodingProtocol=hls` +
|
|
||||||
`&deviceId=sonixd_${deviceId}` +
|
|
||||||
`&playSessionId=${deviceId}` +
|
|
||||||
`&api_key=${token}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const streamUrl = (
|
|
||||||
serverType: string,
|
|
||||||
args: {
|
|
||||||
deviceId: string;
|
|
||||||
remoteId: string;
|
|
||||||
token: string;
|
|
||||||
url: string;
|
|
||||||
userId?: string;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
if (serverType === 'jellyfin') {
|
|
||||||
return getJellyfinStreamUrl(
|
|
||||||
args.remoteId,
|
|
||||||
args.url,
|
|
||||||
args.token,
|
|
||||||
args.userId || '',
|
|
||||||
args.deviceId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverType === 'subsonic') {
|
|
||||||
return getSubsonicStreamUrl(
|
|
||||||
args.remoteId,
|
|
||||||
args.url,
|
|
||||||
args.token,
|
|
||||||
args.deviceId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const relatedAlbum = (item: any) => {
|
|
||||||
return {
|
|
||||||
deleted: item.deleted,
|
|
||||||
id: item.id,
|
|
||||||
itemType: Item.ALBUM,
|
|
||||||
name: item.name,
|
|
||||||
remoteId: item.remoteId,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const relatedArtists = (items: any[]) => {
|
|
||||||
return (
|
|
||||||
items?.map((item: any) => {
|
|
||||||
return {
|
|
||||||
deleted: item.deleted,
|
|
||||||
id: item.id,
|
|
||||||
itemType: Item.ARTIST,
|
|
||||||
name: item.name,
|
|
||||||
remoteId: item.remoteId,
|
|
||||||
};
|
|
||||||
}) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const relatedAlbumArtist = (item: any) => {
|
|
||||||
return {
|
|
||||||
deleted: item.deleted,
|
|
||||||
id: item.id,
|
|
||||||
itemType: item.ALBUMARTIST,
|
|
||||||
name: item.name,
|
|
||||||
remoteId: item.remoteId,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const relatedGenres = (genres: any[]) => {
|
|
||||||
return (
|
|
||||||
genres?.map((genre) => {
|
|
||||||
return {
|
|
||||||
id: genre.id,
|
|
||||||
itemType: Item.GENRE,
|
|
||||||
name: genre.name,
|
|
||||||
};
|
|
||||||
}) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const primaryImage = (
|
|
||||||
images: any[],
|
|
||||||
serverType: string,
|
|
||||||
url: string,
|
|
||||||
remoteId: string
|
|
||||||
) => {
|
|
||||||
const primaryImageId = images.find((i: any) => i.name === 'Primary')?.url;
|
|
||||||
const image = !primaryImageId ? '' : getImageUrl(serverType, url, remoteId);
|
|
||||||
|
|
||||||
return image;
|
|
||||||
};
|
|
||||||
|
|
||||||
const songs = (
|
|
||||||
items: any[],
|
|
||||||
options: {
|
|
||||||
deviceId: string;
|
|
||||||
imageUrl?: string;
|
|
||||||
serverFolderId?: number;
|
|
||||||
serverType?: string;
|
|
||||||
token: string;
|
|
||||||
url?: string;
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
items?.map((item: any) => {
|
|
||||||
const serverType = options.serverType
|
|
||||||
? options?.serverType
|
|
||||||
: item.server.serverType;
|
|
||||||
|
|
||||||
const url = options.url ? options.url : item.server.serverUrls[0];
|
|
||||||
|
|
||||||
return {
|
|
||||||
album: item.album && relatedAlbum(item.album),
|
|
||||||
artistName: item.artistName,
|
|
||||||
artists: relatedArtists(item.artists),
|
|
||||||
bitRate: item.bitRate,
|
|
||||||
container: item.container,
|
|
||||||
createdAt: item.createdAt,
|
|
||||||
date: item.date,
|
|
||||||
deleted: item.deleted,
|
|
||||||
disc: item.disc,
|
|
||||||
duration: item.duration,
|
|
||||||
genres: relatedGenres(item.genres),
|
|
||||||
id: item.id,
|
|
||||||
imageUrl:
|
|
||||||
primaryImage(item.images, serverType, url, item.remoteId) ||
|
|
||||||
options.imageUrl,
|
|
||||||
itemType: Item.SONG,
|
|
||||||
name: item.name,
|
|
||||||
remoteCreatedAt: item.remoteCreatedAt,
|
|
||||||
remoteId: item.remoteId,
|
|
||||||
serverFolderId: item.serverFolderId,
|
|
||||||
serverId: item.serverId,
|
|
||||||
streamUrl: streamUrl(serverType, {
|
|
||||||
deviceId: options.deviceId,
|
|
||||||
remoteId: item.remoteId,
|
|
||||||
token: options.token,
|
|
||||||
url,
|
|
||||||
userId: options.userId,
|
|
||||||
}),
|
|
||||||
track: item.track,
|
|
||||||
updatedAt: item.updatedAt,
|
|
||||||
year: item.year,
|
|
||||||
};
|
|
||||||
}) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const albums = (items: any[], user: User) => {
|
|
||||||
return (
|
|
||||||
items?.map((item: any) => {
|
|
||||||
const { serverType, token, remoteUserId } = item.server;
|
|
||||||
const { url } = item.server.serverUrls[0];
|
|
||||||
const rating = item.ratings.find(
|
|
||||||
(r: Rating) => r.userId === user.id
|
|
||||||
)?.value;
|
|
||||||
const averageRating = meanBy(item.ratings, 'value');
|
|
||||||
const imageUrl = primaryImage(
|
|
||||||
item.images,
|
|
||||||
serverType,
|
|
||||||
url,
|
|
||||||
item.remoteId
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
albumArtist: item.albumArtist && relatedAlbumArtist(item.albumArtist),
|
|
||||||
averageRating,
|
|
||||||
createdAt: item.createdAt,
|
|
||||||
date: item.date,
|
|
||||||
deleted: item.deleted,
|
|
||||||
genres: relatedGenres(item.genres),
|
|
||||||
id: item.id,
|
|
||||||
imageUrl,
|
|
||||||
itemType: Item.ALBUM,
|
|
||||||
name: item.name,
|
|
||||||
rating,
|
|
||||||
remoteCreatedAt: item.remoteCreatedAt,
|
|
||||||
remoteId: item.remoteId,
|
|
||||||
serverFolderId: item.serverFolderId,
|
|
||||||
serverType,
|
|
||||||
songCount: item._count.songs,
|
|
||||||
songs: songs(item.songs, {
|
|
||||||
deviceId: user.deviceId,
|
|
||||||
imageUrl,
|
|
||||||
serverFolderId: item.serverFolderId,
|
|
||||||
serverType,
|
|
||||||
token,
|
|
||||||
url,
|
|
||||||
userId: remoteUserId,
|
|
||||||
}),
|
|
||||||
updatedAt: item.updatedAt,
|
|
||||||
year: item.year,
|
|
||||||
};
|
|
||||||
}) || []
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toRes = {
|
|
||||||
albums,
|
|
||||||
songs,
|
|
||||||
};
|
|
||||||
@@ -1,27 +1,98 @@
|
|||||||
|
import { ServerType, TaskType } from '@prisma/client';
|
||||||
|
import { SortOrder } from '@/types/types';
|
||||||
|
import { helpers } from '../helpers';
|
||||||
import { prisma } from '../lib';
|
import { prisma } from '../lib';
|
||||||
import {
|
import { AuthUser } from '../middleware';
|
||||||
jellyfinApi,
|
import { subsonic } from '../queue';
|
||||||
jellyfinTasks,
|
import { jellyfin } from '../queue/jellyfin';
|
||||||
subsonicApi,
|
import { navidrome } from '../queue/navidrome';
|
||||||
subsonicTasks,
|
import { ApiError } from '../utils';
|
||||||
} from '../queue';
|
|
||||||
import { User } from '../types/types';
|
|
||||||
import { ApiError, ApiSuccess, splitNumberString } from '../utils';
|
|
||||||
|
|
||||||
const findById = async (user: User, options: { id: number }) => {
|
const remoteServerLogin = async (options: {
|
||||||
|
legacy?: boolean;
|
||||||
|
password: string;
|
||||||
|
type: ServerType;
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
}) => {
|
||||||
|
if (options.type === ServerType.JELLYFIN) {
|
||||||
|
const res = await jellyfin.api.authenticate({
|
||||||
|
password: options.password,
|
||||||
|
url: options.url,
|
||||||
|
username: options.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
throw ApiError.badRequest('Invalid credentials.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
remoteUserId: res.User.Id,
|
||||||
|
token: res.AccessToken,
|
||||||
|
type: ServerType.JELLYFIN,
|
||||||
|
url: options.url,
|
||||||
|
username: options.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.type === ServerType.SUBSONIC) {
|
||||||
|
const res = await subsonic.api.authenticate({
|
||||||
|
legacy: options.legacy,
|
||||||
|
password: options.password,
|
||||||
|
url: options.url,
|
||||||
|
username: options.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status === 'failed') {
|
||||||
|
throw ApiError.badRequest('Invalid credentials.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
remoteUserId: '',
|
||||||
|
token: res.token,
|
||||||
|
type: ServerType.SUBSONIC,
|
||||||
|
url: options.url,
|
||||||
|
username: options.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.type === ServerType.NAVIDROME) {
|
||||||
|
const res = await navidrome.api.authenticate({
|
||||||
|
password: options.password,
|
||||||
|
url: options.url,
|
||||||
|
username: options.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
altToken: `u=${res.name}&s=${res.subsonicSalt}&t=${res.subsonicToken}`,
|
||||||
|
remoteUserId: res.id,
|
||||||
|
token: res.token,
|
||||||
|
type: ServerType.NAVIDROME,
|
||||||
|
url: options.url,
|
||||||
|
username: options.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ApiError.badRequest('Server type invalid.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findById = async (user: AuthUser, options: { id: string }) => {
|
||||||
const { id } = options;
|
const { id } = options;
|
||||||
|
|
||||||
|
helpers.shared.checkServerPermissions(user, { serverId: id });
|
||||||
|
|
||||||
const server = await prisma.server.findUnique({
|
const server = await prisma.server.findUnique({
|
||||||
include: {
|
include: {
|
||||||
serverFolders: user.isAdmin
|
serverFolders: user.isAdmin
|
||||||
? true
|
? true
|
||||||
: {
|
: {
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [{ serverFolderPermissions: { some: { userId: user.id } } }],
|
||||||
{ isPublic: true },
|
|
||||||
{ serverFolderPermissions: { some: { userId: user.id } } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
serverPermissions: {
|
||||||
|
where: { userId: user.id },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
@@ -30,108 +101,162 @@ const findById = async (user: User, options: { id: number }) => {
|
|||||||
throw ApiError.notFound('');
|
throw ApiError.notFound('');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.isAdmin && server.serverFolders.length === 0) {
|
return server;
|
||||||
throw ApiError.forbidden('');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: server });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const findMany = async (user: User) => {
|
const findMany = async (user: AuthUser) => {
|
||||||
let servers;
|
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
servers = await prisma.server.findMany({
|
return prisma.server.findMany({
|
||||||
include: { serverFolders: true },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
servers = await prisma.server.findMany({
|
|
||||||
include: {
|
include: {
|
||||||
serverFolders: {
|
serverFolders: {
|
||||||
where: {
|
orderBy: { name: SortOrder.ASC },
|
||||||
OR: [
|
},
|
||||||
{ isPublic: true },
|
serverPermissions: {
|
||||||
{ serverFolderPermissions: { some: { userId: user.id } } },
|
orderBy: { createdAt: SortOrder.ASC },
|
||||||
],
|
where: { userId: user.id },
|
||||||
|
},
|
||||||
|
serverUrls: {
|
||||||
|
include: {
|
||||||
|
userServerUrls: {
|
||||||
|
where: { userId: user.id },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: { serverFolders: { some: { isPublic: true } } },
|
orderBy: { createdAt: SortOrder.ASC },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: servers });
|
const servers = await prisma.server.findMany({
|
||||||
|
include: {
|
||||||
|
serverFolders: {
|
||||||
|
orderBy: { name: SortOrder.ASC },
|
||||||
|
where: { id: { in: user.flatServerFolderPermissions } },
|
||||||
|
},
|
||||||
|
serverPermissions: {
|
||||||
|
orderBy: { createdAt: SortOrder.ASC },
|
||||||
|
where: { userId: user.id },
|
||||||
|
},
|
||||||
|
serverUrls: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: SortOrder.ASC },
|
||||||
|
where: { id: { in: user.flatServerPermissions } },
|
||||||
|
});
|
||||||
|
|
||||||
|
return servers;
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = async (options: {
|
const create = async (options: {
|
||||||
|
altToken?: string; // Used for Navidrome only
|
||||||
name: string;
|
name: string;
|
||||||
remoteUserId: string;
|
remoteUserId: string;
|
||||||
serverType: string;
|
|
||||||
token: string;
|
token: string;
|
||||||
|
type: ServerType;
|
||||||
url: string;
|
url: string;
|
||||||
username: string;
|
username: string;
|
||||||
}) => {
|
}) => {
|
||||||
const checkDuplicate = await prisma.server.findUnique({
|
const isDuplicate = await prisma.server.findUnique({
|
||||||
where: { url: options.url },
|
where: { url: options.url },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (checkDuplicate) {
|
if (isDuplicate) {
|
||||||
throw ApiError.conflict('Server already exists.');
|
throw ApiError.conflict('Server already exists.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let musicFoldersData: {
|
const serverFolders: {
|
||||||
name: string;
|
name: string;
|
||||||
remoteId: string;
|
remoteId: string;
|
||||||
serverId: number;
|
serverId: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
if (options.serverType === 'subsonic') {
|
if (options.type === ServerType.SUBSONIC) {
|
||||||
const musicFoldersRes = await subsonicApi.getMusicFolders({
|
const serverFoldersRes = await subsonic.api.getMusicFolders({
|
||||||
token: options.token,
|
token: options.token,
|
||||||
url: options.url,
|
url: options.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!musicFoldersRes) {
|
if (!serverFoldersRes) {
|
||||||
throw ApiError.badRequest('Server is inaccessible.');
|
throw ApiError.badRequest('Server is inaccessible.');
|
||||||
}
|
}
|
||||||
|
const serverFoldersCreate = serverFoldersRes.map((folder) => {
|
||||||
|
return {
|
||||||
|
name: folder.name,
|
||||||
|
remoteId: String(folder.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = await prisma.server.create({
|
||||||
|
data: {
|
||||||
|
...options,
|
||||||
|
serverFolders: { create: serverFoldersCreate },
|
||||||
|
serverUrls: { create: { url: options.url } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// for (const serverFolder of serverFolders) {
|
||||||
|
// await prisma.serverFolder.upsert({
|
||||||
|
// create: serverFolder,
|
||||||
|
// update: { name: serverFolder.name },
|
||||||
|
// where: {
|
||||||
|
// uniqueServerFolderId: {
|
||||||
|
// remoteId: serverFolder.remoteId,
|
||||||
|
// serverId: serverFolder.serverId,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.type === ServerType.NAVIDROME) {
|
||||||
|
const serverFoldersRes = await subsonic.api.getMusicFolders({
|
||||||
|
token: options.altToken,
|
||||||
|
url: options.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!serverFoldersRes) {
|
||||||
|
throw ApiError.badRequest('Server is inaccessible.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverFoldersCreate = serverFoldersRes.map((folder) => {
|
||||||
|
return {
|
||||||
|
name: folder.name,
|
||||||
|
remoteId: String(folder.id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const server = await prisma.server.create({
|
const server = await prisma.server.create({
|
||||||
data: {
|
data: {
|
||||||
name: options.name,
|
name: options.name,
|
||||||
remoteUserId: options.remoteUserId,
|
remoteUserId: options.remoteUserId,
|
||||||
serverType: options.serverType,
|
serverFolders: { create: serverFoldersCreate },
|
||||||
|
serverUrls: { create: { url: options.url } },
|
||||||
token: options.token,
|
token: options.token,
|
||||||
|
type: options.type,
|
||||||
url: options.url,
|
url: options.url,
|
||||||
username: options.username,
|
username: options.username,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
for (const serverFolder of serverFolders) {
|
||||||
return {
|
|
||||||
name: musicFolder.name,
|
|
||||||
remoteId: String(musicFolder.id),
|
|
||||||
serverId: server.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
musicFoldersData.forEach(async (musicFolder) => {
|
|
||||||
await prisma.serverFolder.upsert({
|
await prisma.serverFolder.upsert({
|
||||||
create: musicFolder,
|
create: serverFolder,
|
||||||
update: { name: musicFolder.name },
|
update: { name: serverFolder.name },
|
||||||
where: {
|
where: {
|
||||||
uniqueServerFolderId: {
|
uniqueServerFolderId: {
|
||||||
remoteId: musicFolder.remoteId,
|
remoteId: serverFolder.remoteId,
|
||||||
serverId: musicFolder.serverId,
|
serverId: serverFolder.serverId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: { ...server } });
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.serverType === 'jellyfin') {
|
if (options.type === ServerType.JELLYFIN) {
|
||||||
const musicFoldersRes = await jellyfinApi.getMusicFolders({
|
const musicFoldersRes = await jellyfin.api.getMusicFolders({
|
||||||
remoteUserId: options.remoteUserId,
|
remoteUserId: options.remoteUserId,
|
||||||
token: options.token,
|
token: options.token,
|
||||||
url: options.url,
|
url: options.url,
|
||||||
@@ -141,62 +266,72 @@ const create = async (options: {
|
|||||||
throw ApiError.badRequest('Server is inaccessible.');
|
throw ApiError.badRequest('Server is inaccessible.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverFoldersCreate = musicFoldersRes.map((musicFolder) => {
|
||||||
|
return {
|
||||||
|
name: musicFolder.Name,
|
||||||
|
remoteId: String(musicFolder.Id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const server = await prisma.server.create({
|
const server = await prisma.server.create({
|
||||||
data: {
|
data: {
|
||||||
name: options.name,
|
name: options.name,
|
||||||
remoteUserId: options.remoteUserId,
|
remoteUserId: options.remoteUserId,
|
||||||
serverType: options.serverType,
|
serverFolders: { create: serverFoldersCreate },
|
||||||
serverUrls: { create: { url: options.url } },
|
serverUrls: { create: { url: options.url } },
|
||||||
token: options.token,
|
token: options.token,
|
||||||
|
type: options.type,
|
||||||
url: options.url,
|
url: options.url,
|
||||||
username: options.username,
|
username: options.username,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
return server;
|
||||||
return {
|
|
||||||
name: musicFolder.Name,
|
|
||||||
remoteId: String(musicFolder.Id),
|
|
||||||
serverId: server.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
musicFoldersData.forEach(async (musicFolder) => {
|
|
||||||
await prisma.serverFolder.upsert({
|
|
||||||
create: musicFolder,
|
|
||||||
update: { name: musicFolder.name },
|
|
||||||
where: {
|
|
||||||
uniqueServerFolderId: {
|
|
||||||
remoteId: musicFolder.remoteId,
|
|
||||||
serverId: musicFolder.serverId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: { ...server } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: {} });
|
throw ApiError.badRequest('Server type invalid.');
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = async (options: { id: number }) => {
|
const update = async (
|
||||||
const { id } = options;
|
options: { id: string },
|
||||||
const server = await prisma.server.findUnique({ where: { id } });
|
data: {
|
||||||
|
altToken?: string; // Used for Navidrome only
|
||||||
|
name?: string;
|
||||||
|
remoteUserId?: string;
|
||||||
|
token?: string;
|
||||||
|
type?: ServerType;
|
||||||
|
url?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return prisma.server.update({
|
||||||
|
data,
|
||||||
|
where: { id: options.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteById = async (options: { id: string }) => {
|
||||||
|
return prisma.server.delete({
|
||||||
|
where: { id: options.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const refresh = async (options: { id: string }) => {
|
||||||
|
const server = await prisma.server.findUnique({ where: { id: options.id } });
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
throw ApiError.notFound('');
|
throw ApiError.notFound('');
|
||||||
}
|
}
|
||||||
|
|
||||||
let musicFoldersData: {
|
let serverFolders: {
|
||||||
name: string;
|
name: string;
|
||||||
remoteId: string;
|
remoteId: string;
|
||||||
serverId: number;
|
serverId: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
if (server.serverType === 'subsonic') {
|
if (server.type === ServerType.SUBSONIC) {
|
||||||
const musicFoldersRes = await subsonicApi.getMusicFolders(server);
|
const serverFoldersRes = await subsonic.api.getMusicFolders(server);
|
||||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
serverFolders = serverFoldersRes.map((musicFolder) => {
|
||||||
return {
|
return {
|
||||||
name: musicFolder.name,
|
name: musicFolder.name,
|
||||||
remoteId: String(musicFolder.id),
|
remoteId: String(musicFolder.id),
|
||||||
@@ -205,9 +340,9 @@ const refresh = async (options: { id: number }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.serverType === 'jellyfin') {
|
if (server.type === ServerType.JELLYFIN) {
|
||||||
const musicFoldersRes = await jellyfinApi.getMusicFolders(server);
|
const musicFoldersRes = await jellyfin.api.getMusicFolders(server);
|
||||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
serverFolders = musicFoldersRes.map((musicFolder) => {
|
||||||
return {
|
return {
|
||||||
name: musicFolder.Name,
|
name: musicFolder.Name,
|
||||||
remoteId: String(musicFolder.Id),
|
remoteId: String(musicFolder.Id),
|
||||||
@@ -217,29 +352,24 @@ const refresh = async (options: { id: number }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mark as deleted if not found
|
// mark as deleted if not found
|
||||||
|
for (const serverFolder of serverFolders) {
|
||||||
musicFoldersData.forEach(async (musicFolder) => {
|
|
||||||
await prisma.serverFolder.upsert({
|
await prisma.serverFolder.upsert({
|
||||||
create: musicFolder,
|
create: serverFolder,
|
||||||
update: { name: musicFolder.name },
|
update: { name: serverFolder.name },
|
||||||
where: {
|
where: {
|
||||||
uniqueServerFolderId: {
|
uniqueServerFolderId: {
|
||||||
remoteId: musicFolder.remoteId,
|
remoteId: serverFolder.remoteId,
|
||||||
serverId: musicFolder.serverId,
|
serverId: serverFolder.serverId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: { ...server } });
|
return server;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fullScan = async (options: {
|
const fullScan = async (options: { id: string; serverFolderId?: string[] }) => {
|
||||||
id: number;
|
const { id, serverFolderId } = options;
|
||||||
serverFolderIds?: string;
|
|
||||||
userId: number;
|
|
||||||
}) => {
|
|
||||||
const { id, serverFolderIds } = options;
|
|
||||||
const server = await prisma.server.findUnique({
|
const server = await prisma.server.findUnique({
|
||||||
include: { serverFolders: true },
|
include: { serverFolders: true },
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -250,52 +380,183 @@ const fullScan = async (options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let serverFolders;
|
let serverFolders;
|
||||||
if (serverFolderIds) {
|
if (serverFolderId) {
|
||||||
const selectedServerFolderIds = splitNumberString(serverFolderIds);
|
serverFolders = server.serverFolders.filter((f) =>
|
||||||
serverFolders = server.serverFolders.filter((folder) =>
|
serverFolderId?.includes(f.id)
|
||||||
selectedServerFolderIds?.includes(folder.id)
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
serverFolders = server.serverFolders;
|
serverFolders = server.serverFolders;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.serverType === 'jellyfin') {
|
if (serverFolders.length === 0) {
|
||||||
for (const serverFolder of serverFolders) {
|
throw ApiError.notFound('No matching server folders found.');
|
||||||
const task = await prisma.task.create({
|
|
||||||
data: {
|
|
||||||
completed: false,
|
|
||||||
inProgress: true,
|
|
||||||
name: 'Full scan',
|
|
||||||
serverFolderId: serverFolder.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await jellyfinTasks.scanAll(server, serverFolder, task);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (server.serverType === 'subsonic') {
|
const task = await prisma.task.create({
|
||||||
for (const serverFolder of serverFolders) {
|
data: {
|
||||||
const task = await prisma.task.create({
|
completed: false,
|
||||||
data: {
|
name: 'Full scan',
|
||||||
completed: false,
|
server: { connect: { id: server.id } },
|
||||||
inProgress: true,
|
type: TaskType.FULL_SCAN,
|
||||||
name: 'Full scan',
|
},
|
||||||
serverFolderId: serverFolder.id,
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await subsonicTasks.scanAll(server, serverFolder, task);
|
if (server.type === ServerType.JELLYFIN) {
|
||||||
}
|
await jellyfin.scanner.scanAll(server, serverFolders, task);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: {} });
|
if (server.type === ServerType.SUBSONIC) {
|
||||||
|
await subsonic.scanner.scanAll(server, serverFolders, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.type === ServerType.NAVIDROME) {
|
||||||
|
await navidrome.scanner.scanAll(server, serverFolders, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const findServerUrlById = async (options: { id: string }) => {
|
||||||
|
const serverUrl = await prisma.serverUrl.findUnique({
|
||||||
|
where: { id: options.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const serverUrl = await prisma.serverUrl.create({
|
||||||
|
data: {
|
||||||
|
serverId,
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return serverUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findUrlById = async (options: { id: string }) => {
|
||||||
|
const url = await prisma.serverUrl.findUnique({
|
||||||
|
where: { id: options.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw ApiError.notFound('Url not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUrlById = async (options: { id: string }) => {
|
||||||
|
await prisma.serverUrl.delete({
|
||||||
|
where: { id: options.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableUrlById = async (
|
||||||
|
user: AuthUser,
|
||||||
|
options: { id: string; serverId: string }
|
||||||
|
) => {
|
||||||
|
await prisma.userServerUrl.deleteMany({ where: { userId: user.id } });
|
||||||
|
await prisma.userServerUrl.create({
|
||||||
|
data: {
|
||||||
|
serverId: options.serverId,
|
||||||
|
serverUrlId: options.id,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableUrlById = async (user: AuthUser) => {
|
||||||
|
await prisma.userServerUrl.deleteMany({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serversService = {
|
export const serversService = {
|
||||||
create,
|
create,
|
||||||
|
createUrl,
|
||||||
|
deleteById,
|
||||||
|
deleteUrlById,
|
||||||
|
disableUrlById,
|
||||||
|
enableUrlById,
|
||||||
findById,
|
findById,
|
||||||
findMany,
|
findMany,
|
||||||
|
findServerUrlById,
|
||||||
|
findUrlById,
|
||||||
fullScan,
|
fullScan,
|
||||||
refresh,
|
refresh,
|
||||||
|
remoteServerLogin,
|
||||||
|
update,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
|
import { User } from '@prisma/client';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { prisma } from '../lib';
|
import { prisma } from '../lib';
|
||||||
import { User } from '../types/types';
|
import { SortOrder } from '../types/types';
|
||||||
import {
|
import { ApiError, ApiSuccess, folderPermissions } from '../utils';
|
||||||
ApiError,
|
// import { toRes } from './response';
|
||||||
ApiSuccess,
|
|
||||||
folderPermissions,
|
|
||||||
splitNumberString,
|
|
||||||
} from '../utils';
|
|
||||||
import { toRes } from './response';
|
|
||||||
import { SongRequestParams } from './types';
|
import { SongRequestParams } from './types';
|
||||||
|
|
||||||
const findById = async (options: { id: number; user: User }) => {
|
const findById = async (options: { id: string; user: User }) => {
|
||||||
const { id, user } = options;
|
const { id } = options;
|
||||||
|
|
||||||
const album = await prisma.album.findUnique({
|
const album = await prisma.album.findUnique({
|
||||||
include: {
|
include: {
|
||||||
_count: true,
|
_count: true,
|
||||||
albumArtist: true,
|
albumArtists: true,
|
||||||
genres: true,
|
genres: true,
|
||||||
songs: {
|
songs: {
|
||||||
include: {
|
include: {
|
||||||
@@ -26,15 +22,16 @@ const findById = async (options: { id: number; user: User }) => {
|
|||||||
genres: true,
|
genres: true,
|
||||||
images: true,
|
images: true,
|
||||||
},
|
},
|
||||||
orderBy: [{ disc: 'asc' }, { track: 'asc' }],
|
orderBy: [
|
||||||
|
{ discNumber: SortOrder.ASC },
|
||||||
|
{ trackNumber: SortOrder.ASC },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!album) {
|
if (!album) throw ApiError.notFound('');
|
||||||
throw ApiError.notFound('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (!(await folderPermissions([album?.serverFolderId], user))) {
|
// if (!(await folderPermissions([album?.serverFolderId], user))) {
|
||||||
// throw ApiError.forbidden('');
|
// throw ApiError.forbidden('');
|
||||||
@@ -49,37 +46,37 @@ const findMany = async (
|
|||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
albumIds: rawAlbumIds,
|
albumIds: rawAlbumIds,
|
||||||
artistIds: rawArtistIds,
|
// artistIds: rawArtistIds,
|
||||||
|
serverId,
|
||||||
songIds: rawSongIds,
|
songIds: rawSongIds,
|
||||||
user,
|
user,
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
serverFolderIds: rServerFolderIds,
|
serverFolderIds: rServerFolderIds,
|
||||||
} = options;
|
} = options;
|
||||||
const serverFolderIds = splitNumberString(rServerFolderIds);
|
const serverFolderIds = rServerFolderIds.split(',');
|
||||||
const albumIds = splitNumberString(rawAlbumIds);
|
const albumIds = rawAlbumIds && rawAlbumIds.split(',');
|
||||||
const artistIds = splitNumberString(rawArtistIds);
|
// const artistIds = rawArtistIds && rawArtistIds.split(',');
|
||||||
const songIds = splitNumberString(rawSongIds);
|
const songIds = rawSongIds && rawSongIds.split(',');
|
||||||
|
|
||||||
if (serverFolderIds) {
|
if (serverFolderIds) {
|
||||||
if (!(await folderPermissions(serverFolderIds, user))) {
|
if (!(await folderPermissions(serverFolderIds, user)))
|
||||||
throw ApiError.forbidden('');
|
throw ApiError.forbidden('');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
// const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
||||||
// return { serverFolders: { id: { equals: serverFolderId } } };
|
// return { serverFolders: { id: { equals: serverFolderId } } };
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const serverFoldersFilter = {
|
// const serverFoldersFilter = {
|
||||||
serverFolders: { some: { id: { in: serverFolderIds } } },
|
// serverFolders: { some: { id: { in: serverFolderIds } } },
|
||||||
};
|
// };
|
||||||
|
|
||||||
const [totalEntries, songs] = await prisma.$transaction([
|
const [totalEntries, songs] = await prisma.$transaction([
|
||||||
prisma.song.count({
|
prisma.song.count({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
serverFoldersFilter,
|
// serverFoldersFilter,
|
||||||
{
|
{
|
||||||
albumId: { in: albumIds },
|
albumId: { in: albumIds },
|
||||||
id: { in: songIds },
|
id: { in: songIds },
|
||||||
@@ -96,19 +93,16 @@ const findMany = async (
|
|||||||
},
|
},
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
where: { OR: serverFoldersFilter },
|
where: {
|
||||||
|
AND: {
|
||||||
|
// OR: serverFoldersFilter,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return ApiSuccess.ok({
|
return { data: songs, totalEntries };
|
||||||
data: songs,
|
|
||||||
paginationItems: {
|
|
||||||
skip,
|
|
||||||
take,
|
|
||||||
totalEntries,
|
|
||||||
url: req.originalUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const songsService = {
|
export const songsService = {
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export interface SongRequestParams extends OffsetPagination {
|
|||||||
albumIds?: string;
|
albumIds?: string;
|
||||||
artistIds?: string;
|
artistIds?: string;
|
||||||
serverFolderIds: string;
|
serverFolderIds: string;
|
||||||
|
serverId: string;
|
||||||
songIds?: string;
|
songIds?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,32 @@
|
|||||||
import { prisma, exclude } from '../lib';
|
import { prisma } from '../lib';
|
||||||
import { ApiError, ApiSuccess } from '../utils';
|
import { AuthUser } from '../middleware';
|
||||||
|
import { ApiError } from '../utils';
|
||||||
|
|
||||||
const getOne = async (options: { id: number }) => {
|
const findById = async (user: AuthUser, options: { id: string }) => {
|
||||||
const { id } = options;
|
const { id } = options;
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
include: {
|
if (!user.isAdmin && user.id !== id) {
|
||||||
serverFolderPermissions: true,
|
throw ApiError.forbidden();
|
||||||
},
|
}
|
||||||
|
|
||||||
|
const uniqueUser = await prisma.user.findUnique({
|
||||||
|
include: { serverFolderPermissions: true },
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!uniqueUser) {
|
||||||
throw ApiError.notFound('');
|
throw ApiError.notFound('');
|
||||||
}
|
}
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: exclude(user, 'password') });
|
return uniqueUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMany = async () => {
|
const findMany = async () => {
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({});
|
||||||
select: {
|
return users;
|
||||||
createdAt: true,
|
|
||||||
enabled: true,
|
|
||||||
id: true,
|
|
||||||
isAdmin: true,
|
|
||||||
serverFolderPermissions: true,
|
|
||||||
updatedAt: true,
|
|
||||||
username: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return ApiSuccess.ok({ data: users });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usersService = {
|
export const usersService = {
|
||||||
getMany,
|
findById,
|
||||||
getOne,
|
findMany,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { User } from '@prisma/client';
|
||||||
import { prisma } from '../lib';
|
import { prisma } from '../lib';
|
||||||
import { User } from '../types/types';
|
|
||||||
|
|
||||||
export enum Roles {
|
export enum Roles {
|
||||||
NONE = 0,
|
NONE = 0,
|
||||||
@@ -24,12 +24,8 @@ export const folderPermissions = async (serverFolderIds: any[], user: User) => {
|
|||||||
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
AND: [
|
AND: [
|
||||||
{ isPublic: false },
|
|
||||||
{
|
{
|
||||||
serverFolderPermissions: {
|
serverFolderPermissions: {
|
||||||
some: { userId: { equals: user.id } },
|
some: { userId: { equals: user.id } },
|
||||||
@@ -66,12 +62,8 @@ export const getFolderPermissions = async (user: User) => {
|
|||||||
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{
|
|
||||||
isPublic: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
AND: [
|
AND: [
|
||||||
{ isPublic: false },
|
|
||||||
{
|
{
|
||||||
serverFolderPermissions: {
|
serverFolderPermissions: {
|
||||||
some: { userId: { equals: user.id } },
|
some: { userId: { equals: user.id } },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { paginationValidation, idValidation } from './shared.validation';
|
import { idValidation, paginationValidation } from './shared.validation';
|
||||||
|
|
||||||
export const list = {
|
export const list = {
|
||||||
body: z.object({}),
|
body: z.object({}),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AlbumSort } from '../helpers/albums.helpers';
|
import { AlbumSort } from '@helpers/albums.helpers';
|
||||||
import {
|
import {
|
||||||
idValidation,
|
idValidation,
|
||||||
orderByValidation,
|
orderByValidation,
|
||||||
@@ -22,13 +22,32 @@ const list = {
|
|||||||
|
|
||||||
const detail = {
|
const detail = {
|
||||||
body: z.object({}),
|
body: z.object({}),
|
||||||
params: z.object({ ...idValidation('id') }),
|
params: z.object({
|
||||||
|
...idValidation('albumId'),
|
||||||
|
...idValidation('serverId'),
|
||||||
|
}),
|
||||||
query: z.object({
|
query: z.object({
|
||||||
...serverUrlIdValidation,
|
...serverUrlIdValidation,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const detailSongList = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({
|
||||||
|
...idValidation('albumId'),
|
||||||
|
...idValidation('serverId'),
|
||||||
|
}),
|
||||||
|
query: z.object({
|
||||||
|
...paginationValidation,
|
||||||
|
...serverFolderIdValidation,
|
||||||
|
...orderByValidation,
|
||||||
|
...serverUrlIdValidation,
|
||||||
|
sortBy: z.nativeEnum(AlbumSort),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
export const albumsValidation = {
|
export const albumsValidation = {
|
||||||
detail,
|
detail,
|
||||||
|
detailSongList,
|
||||||
list,
|
list,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AlbumSort } from '../helpers/albums.helpers';
|
import { AlbumSort } from '@helpers/albums.helpers';
|
||||||
import {
|
import {
|
||||||
idValidation,
|
idValidation,
|
||||||
orderByValidation,
|
orderByValidation,
|
||||||
|
|||||||
@@ -4,7 +4,32 @@ import { idValidation } from './shared.validation';
|
|||||||
|
|
||||||
const detail = {
|
const detail = {
|
||||||
body: z.object({}),
|
body: z.object({}),
|
||||||
params: z.object({ ...idValidation('id') }),
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const list = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({}),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteServer = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = {
|
||||||
|
body: z.object({
|
||||||
|
legacy: z.boolean().optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
password: z.string().optional(),
|
||||||
|
type: z.nativeEnum(ServerType),
|
||||||
|
url: z.string().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
}),
|
||||||
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
query: z.object({}),
|
query: z.object({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,26 +52,103 @@ const create = {
|
|||||||
|
|
||||||
const scan = {
|
const scan = {
|
||||||
body: z.object({ serverFolderId: z.string().array().optional() }),
|
body: z.object({ serverFolderId: z.string().array().optional() }),
|
||||||
params: z.object({ ...idValidation('id') }),
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
query: z.object({}),
|
query: z.object({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = {
|
const refresh = {
|
||||||
body: z.object({}),
|
body: z.object({}),
|
||||||
params: z.object({ ...idValidation('id') }),
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
query: z.object({}),
|
query: z.object({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const createCredential = {
|
const createCredential = {
|
||||||
body: z.object({ credential: z.string() }),
|
body: z.object({ credential: z.string(), username: z.string() }),
|
||||||
params: z.object({ ...idValidation('id') }),
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCredentialDetail = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCredential = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({
|
||||||
|
...idValidation('serverId'),
|
||||||
|
...idValidation('credentialId'),
|
||||||
|
}),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableCredential = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({
|
||||||
|
...idValidation('serverId'),
|
||||||
|
...idValidation('credentialId'),
|
||||||
|
}),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableCredential = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({
|
||||||
|
...idValidation('serverId'),
|
||||||
|
...idValidation('credentialId'),
|
||||||
|
}),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUrl = {
|
||||||
|
body: z.object({ url: z.string() }),
|
||||||
|
params: z.object({ ...idValidation('serverId') }),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteUrl = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({
|
||||||
|
...idValidation('serverId'),
|
||||||
|
...idValidation('urlId'),
|
||||||
|
}),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableUrl = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({
|
||||||
|
...idValidation('serverId'),
|
||||||
|
...idValidation('urlId'),
|
||||||
|
}),
|
||||||
|
query: z.object({}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableUrl = {
|
||||||
|
body: z.object({}),
|
||||||
|
params: z.object({
|
||||||
|
...idValidation('serverId'),
|
||||||
|
...idValidation('urlId'),
|
||||||
|
}),
|
||||||
query: z.object({}),
|
query: z.object({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serversValidation = {
|
export const serversValidation = {
|
||||||
create,
|
create,
|
||||||
createCredential,
|
createCredential,
|
||||||
|
createUrl,
|
||||||
|
deleteCredential,
|
||||||
|
deleteServer,
|
||||||
|
deleteUrl,
|
||||||
detail,
|
detail,
|
||||||
|
disableCredential,
|
||||||
|
disableUrl,
|
||||||
|
enableCredential,
|
||||||
|
enableUrl,
|
||||||
|
getCredentialDetail,
|
||||||
|
list,
|
||||||
refresh,
|
refresh,
|
||||||
scan,
|
scan,
|
||||||
|
update,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Modified from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware
|
|
||||||
import { Request, RequestHandler } from 'express';
|
import { Request, RequestHandler } from 'express';
|
||||||
import { z, ZodError, ZodSchema } from 'zod';
|
import { z, ZodError, ZodSchema } from 'zod';
|
||||||
import { SortOrder } from '../types/types';
|
import { SortOrder } from '@/types/types';
|
||||||
import { ApiError } from '../utils';
|
import { ApiError } from '@/utils';
|
||||||
|
// Modified from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware
|
||||||
|
|
||||||
export type TypedRequest<
|
export type TypedRequest<
|
||||||
S extends {
|
S extends {
|
||||||
|
|||||||
Reference in New Issue
Block a user