mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Redo server functionality
This commit is contained in:
@@ -1,41 +1,45 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { albumArtistsService } from '../services';
|
||||
import {
|
||||
getSuccessResponse,
|
||||
idValidation,
|
||||
paginationValidation,
|
||||
validateRequest,
|
||||
} from '../utils';
|
||||
|
||||
const getAlbumArtists = async (req: Request, res: Response) => {
|
||||
validateRequest(req, {
|
||||
query: z.object({
|
||||
...paginationValidation,
|
||||
serverFolderIds: z.string().min(1),
|
||||
}),
|
||||
});
|
||||
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||
import { service } from '@services/index';
|
||||
import { validation, TypedRequest } from '@validations/index';
|
||||
|
||||
const getList = async (req: Request, res: Response) => {
|
||||
const { take, skip, serverFolderIds } = req.query;
|
||||
const data = await albumArtistsService.findMany(req, {
|
||||
const albumArtists = await service.albumArtists.findMany(req, {
|
||||
serverFolderIds: String(serverFolderIds),
|
||||
skip: Number(skip),
|
||||
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) => {
|
||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
||||
|
||||
const getDetail = async (
|
||||
req: TypedRequest<typeof validation.albumArtists.detail>,
|
||||
res: Response
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const data = await albumArtistsService.findById({
|
||||
id: Number(id),
|
||||
user: req.auth,
|
||||
const albumArtist = await service.albumArtists.findById({
|
||||
id,
|
||||
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 { z } from 'zod';
|
||||
import { AlbumSort } from '../helpers/albums.helpers';
|
||||
import { albumsService } from '../services';
|
||||
import { SortOrder } from '../types/types';
|
||||
import {
|
||||
getSuccessResponse,
|
||||
idValidation,
|
||||
paginationValidation,
|
||||
validateRequest,
|
||||
} from '../utils';
|
||||
import { Response } from 'express';
|
||||
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||
import { toApiModel } from '@helpers/api-model';
|
||||
import { service } from '@services/index';
|
||||
import { TypedRequest, validation } from '@validations/index';
|
||||
|
||||
const getAlbumById = async (req: Request, res: Response) => {
|
||||
validateRequest(req, {
|
||||
params: z.object({ ...idValidation }),
|
||||
query: z.object({ serverUrls: z.optional(z.string().min(1)) }),
|
||||
const getDetail = async (
|
||||
req: TypedRequest<typeof validation.albums.detail>,
|
||||
res: Response
|
||||
) => {
|
||||
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;
|
||||
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));
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const getAlbums = async (req: Request, res: Response) => {
|
||||
validateRequest(req, {
|
||||
query: z.object({
|
||||
...paginationValidation,
|
||||
orderBy: z.nativeEnum(SortOrder),
|
||||
serverFolderIds: z.optional(z.string().min(1)),
|
||||
serverUrls: z.optional(z.string().min(1)),
|
||||
sortBy: z.nativeEnum(AlbumSort),
|
||||
}),
|
||||
});
|
||||
const getList = async (
|
||||
req: TypedRequest<typeof validation.albums.list>,
|
||||
res: Response
|
||||
) => {
|
||||
const { serverId } = req.params;
|
||||
const { take, skip, serverUrlId } = req.query;
|
||||
|
||||
const { take, serverFolderIds, serverUrls, sortBy, orderBy, skip } =
|
||||
req.query;
|
||||
|
||||
const data = await albumsService.findMany(req, {
|
||||
orderBy: orderBy as SortOrder,
|
||||
serverFolderIds: serverFolderIds && String(serverFolderIds),
|
||||
serverUrls: serverUrls && String(serverUrls),
|
||||
const albums = await service.albums.findMany({
|
||||
...req.query,
|
||||
serverId,
|
||||
skip: Number(skip),
|
||||
sortBy: sortBy as AlbumSort,
|
||||
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 = {
|
||||
getAlbumById,
|
||||
getAlbums,
|
||||
getDetail,
|
||||
getDetailSongList,
|
||||
getList,
|
||||
};
|
||||
|
||||
@@ -1,41 +1,50 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { artistsService } from '../services';
|
||||
import {
|
||||
getSuccessResponse,
|
||||
idValidation,
|
||||
paginationValidation,
|
||||
validateRequest,
|
||||
} from '../utils';
|
||||
|
||||
const getArtistById = async (req: Request, res: Response) => {
|
||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
||||
import { Response } from 'express';
|
||||
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||
import { service } from '@services/index';
|
||||
import { validation, TypedRequest } from '@validations/index';
|
||||
|
||||
const getDetail = async (
|
||||
req: TypedRequest<typeof validation.artists.detail>,
|
||||
res: Response
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const data = await artistsService.findById({
|
||||
id: Number(id),
|
||||
user: req.auth,
|
||||
|
||||
const artist = await service.artists.findById({
|
||||
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) => {
|
||||
validateRequest(req, {
|
||||
query: z.object({
|
||||
...paginationValidation,
|
||||
serverFolderIds: z.string().min(1),
|
||||
}),
|
||||
});
|
||||
const getList = async (
|
||||
req: TypedRequest<typeof validation.artists.list>,
|
||||
res: Response
|
||||
) => {
|
||||
const { take, skip, serverFolderId } = req.query;
|
||||
|
||||
const { take, skip, serverFolderIds } = req.query;
|
||||
const data = await artistsService.findMany(req, {
|
||||
serverFolderIds: String(serverFolderIds),
|
||||
skip: Number(skip),
|
||||
take: Number(take),
|
||||
user: req.auth,
|
||||
});
|
||||
// const artists = await service.artists.findMany(req, {
|
||||
// serverFolderIds: String(serverFolderIds),
|
||||
// skip: Number(skip),
|
||||
// take: Number(take),
|
||||
// user: req.authUser,
|
||||
// });
|
||||
|
||||
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 { 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 { 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 { 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) => {
|
||||
validateRequest(req, {
|
||||
body: z.object({
|
||||
password: z.string().min(6).max(255),
|
||||
username: z.string().min(4).max(26),
|
||||
}),
|
||||
});
|
||||
|
||||
const register = async (
|
||||
req: TypedRequest<typeof validation.auth.register>,
|
||||
res: Response
|
||||
) => {
|
||||
const { username, password } = req.body;
|
||||
const { statusCode, data } = await authService.register({
|
||||
const user = await service.auth.register({
|
||||
password,
|
||||
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 { statusCode, data } = await authService.logout({ user: req.auth });
|
||||
return res.status(statusCode).json(getSuccessResponse({ data, statusCode }));
|
||||
await service.auth.logout({
|
||||
user: req.authUser,
|
||||
});
|
||||
|
||||
const success = ApiSuccess.noContent({ data: {} });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
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) => {
|
||||
validateRequest(req, {
|
||||
body: z.object({
|
||||
refreshToken: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
const { data, statusCode } = await authService.refresh({
|
||||
const refresh = async (
|
||||
req: TypedRequest<typeof validation.auth.refresh>,
|
||||
res: Response
|
||||
) => {
|
||||
const refresh = await service.auth.refresh({
|
||||
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 };
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
export * from './album-artists.controller';
|
||||
export * from './auth.controller';
|
||||
export * from './servers.controller';
|
||||
export * from './users.controller';
|
||||
export * from './artists.controller';
|
||||
export * from './albums.controller';
|
||||
import { albumArtistsController } from './album-artists.controller';
|
||||
import { albumsController } from './albums.controller';
|
||||
import { artistsController } from './artists.controller';
|
||||
import { authController } from './auth.controller';
|
||||
import { serversController } from './servers.controller';
|
||||
import { songsController } from './songs.controller';
|
||||
import { usersController } from './users.controller';
|
||||
|
||||
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 { z } from 'zod';
|
||||
import { prisma } from '../lib';
|
||||
import { serversService } from '../services';
|
||||
import { getSuccessResponse, idValidation, validateRequest } from '../utils';
|
||||
import { Response } from 'express';
|
||||
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||
import { toApiModel } from '@helpers/api-model';
|
||||
import { service } from '@services/index';
|
||||
import { TypedRequest, validation } from '@validations/index';
|
||||
|
||||
const getServerById = async (req: Request, res: Response) => {
|
||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
||||
const getServerDetail = async (
|
||||
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 data = await serversService.findById(req.auth, {
|
||||
id: Number(id),
|
||||
const getServerList = async (
|
||||
req: TypedRequest<typeof validation.servers.list>,
|
||||
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 data = await serversService.findMany(req.auth);
|
||||
const updateServer = async (
|
||||
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 data = await serversService.create(req.body);
|
||||
const refreshServer = async (
|
||||
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 { id } = req.params;
|
||||
const data = await serversService.refresh({ id: Number(id) });
|
||||
const scanServer = async (
|
||||
req: TypedRequest<typeof validation.servers.scan>,
|
||||
res: Response
|
||||
) => {
|
||||
const { serverId } = req.params;
|
||||
const { serverFolderId } = req.body;
|
||||
|
||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
||||
};
|
||||
|
||||
const scanServer = async (req: Request, res: Response) => {
|
||||
validateRequest(req, {
|
||||
query: z.object({ serverFolderIds: z.string().optional() }),
|
||||
const data = await service.servers.fullScan({
|
||||
id: serverId,
|
||||
serverFolderId,
|
||||
});
|
||||
|
||||
const { id } = req.params;
|
||||
const { serverFolderIds } = req.query;
|
||||
|
||||
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 success = ApiSuccess.ok({ data });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const getFolder = async (req: Request, res: Response) => {
|
||||
const data = await prisma.folder.findUnique({
|
||||
include: {
|
||||
children: true,
|
||||
},
|
||||
where: { id: Number(req.params.id) },
|
||||
const createServerUrl = async (
|
||||
req: TypedRequest<typeof validation.servers.createUrl>,
|
||||
res: Response
|
||||
) => {
|
||||
const { serverId } = req.params;
|
||||
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 = {
|
||||
createServer,
|
||||
getFolder,
|
||||
getServerById,
|
||||
getServers,
|
||||
createServerUrl,
|
||||
deleteServer,
|
||||
deleteServerUrl,
|
||||
disableServerUrl,
|
||||
enableServerUrl,
|
||||
getServerDetail,
|
||||
getServerList,
|
||||
refreshServer,
|
||||
scanServer,
|
||||
updateServer,
|
||||
};
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
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) => {
|
||||
validateRequest(req, {
|
||||
query: z.object({
|
||||
...paginationValidation,
|
||||
albumIds: z.optional(z.string()),
|
||||
artistIds: z.optional(z.string()),
|
||||
serverFolderIds: z.optional(z.string().min(1)),
|
||||
songIds: z.optional(z.string()),
|
||||
}),
|
||||
});
|
||||
const getSongList = async (req: Request, res: Response) => {
|
||||
const { serverId } = req.params;
|
||||
const { take, skip, serverFolderId } = req.query;
|
||||
|
||||
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, {
|
||||
serverFolderIds: String(serverFolderIds),
|
||||
skip: Number(skip),
|
||||
take: Number(take),
|
||||
user: req.auth,
|
||||
});
|
||||
// const success = ApiSuccess.ok({
|
||||
// // data: toRes.songs(songs.data, req.authUser),
|
||||
// data: songs.data,
|
||||
// paginationItems: {
|
||||
// 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 = {
|
||||
getSongs,
|
||||
getSongList,
|
||||
};
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { usersService } from '../services';
|
||||
import { getSuccessResponse, idValidation, validateRequest } from '../utils';
|
||||
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||
import { toApiModel } from '@helpers/api-model';
|
||||
import { service } from '@services/index';
|
||||
|
||||
const getUser = async (req: Request, res: Response) => {
|
||||
validateRequest(req, { params: z.object({ ...idValidation }) });
|
||||
const getUserDetail = async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
const data = await usersService.getOne({ id: Number(id) });
|
||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
||||
const user = await service.users.findById(req.authUser, { id });
|
||||
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 data = await usersService.getMany();
|
||||
return res.status(data.statusCode).json(getSuccessResponse(data));
|
||||
const getUserList = async (_req: Request, res: Response) => {
|
||||
const users = await service.users.findMany();
|
||||
const success = ApiSuccess.ok({ data: toApiModel.users(users) });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
export const usersController = {
|
||||
getUser,
|
||||
getUsers,
|
||||
getUserDetail,
|
||||
getUserList,
|
||||
};
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { SortOrder } from '../types/types';
|
||||
import { splitNumberString } from '../utils';
|
||||
import { songHelpers } from './songs.helpers';
|
||||
import { AuthUser } from '@/middleware';
|
||||
import { SortOrder } from '@/types/types';
|
||||
import { songHelpers } from '@helpers/songs.helpers';
|
||||
|
||||
export enum AlbumSort {
|
||||
DATE_ADDED = 'date_added',
|
||||
DATE_ADDED_REMOTE = 'date_added_remote',
|
||||
DATE_RELEASED = 'date_released',
|
||||
DATE_ADDED = 'added',
|
||||
DATE_ADDED_REMOTE = 'addedRemote',
|
||||
DATE_RELEASED = 'released',
|
||||
DATE_RELEASED_YEAR = 'year',
|
||||
FAVORITE = 'favorite',
|
||||
NAME = 'name',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
TITLE = 'title',
|
||||
YEAR = 'year',
|
||||
}
|
||||
|
||||
const include = (options?: { serverUrls?: string; songs: boolean }) => {
|
||||
const props: Prisma.AlbumInclude = {
|
||||
_count: { select: { favorites: true, songs: true } },
|
||||
albumArtist: true,
|
||||
genres: true,
|
||||
images: true,
|
||||
ratings: true,
|
||||
server: {
|
||||
include: {
|
||||
serverUrls: options?.serverUrls
|
||||
? { where: { id: { in: splitNumberString(options.serverUrls) } } }
|
||||
: true,
|
||||
const include = (options: { songs?: boolean; user?: AuthUser }) => {
|
||||
// Prisma.AlbumInclude
|
||||
const props = {
|
||||
_count: {
|
||||
select: {
|
||||
favorites: true,
|
||||
songs: true,
|
||||
},
|
||||
},
|
||||
|
||||
songs: options?.songs ? songHelpers.include() : false,
|
||||
albumArtists: true,
|
||||
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;
|
||||
@@ -38,7 +44,7 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => {
|
||||
let order;
|
||||
|
||||
switch (sortBy) {
|
||||
case AlbumSort.TITLE:
|
||||
case AlbumSort.NAME:
|
||||
order = { name: orderBy };
|
||||
break;
|
||||
|
||||
@@ -51,11 +57,19 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => {
|
||||
break;
|
||||
|
||||
case AlbumSort.DATE_RELEASED:
|
||||
order = { date: orderBy, year: orderBy };
|
||||
order = { releaseDate: orderBy, year: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.YEAR:
|
||||
order = { year: orderBy };
|
||||
case AlbumSort.DATE_RELEASED_YEAR:
|
||||
order = { releaseYear: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.RATING:
|
||||
order = { rating: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.FAVORITE:
|
||||
order = { favorite: orderBy };
|
||||
break;
|
||||
|
||||
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[]) => {
|
||||
return serverFolderIds!.map((serverFolderId: number) => {
|
||||
return {
|
||||
serverFolders: { some: { id: { equals: Number(serverFolderId) } } },
|
||||
};
|
||||
import { AuthUser } from '@/middleware';
|
||||
import { ApiError } from '@/utils';
|
||||
import { prisma } from '@lib/prisma';
|
||||
|
||||
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 = {
|
||||
checkServerFolderPermissions,
|
||||
checkServerPermissions,
|
||||
getAvailableServerFolderIds,
|
||||
params: {
|
||||
pagination: paginationParams,
|
||||
},
|
||||
serverFolderFilter,
|
||||
};
|
||||
|
||||
@@ -1,20 +1,45 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
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: {
|
||||
album: true,
|
||||
artists: true,
|
||||
externals: true,
|
||||
genres: 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 = {
|
||||
findMany,
|
||||
include,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
ServerCredential,
|
||||
ServerFolderPermission,
|
||||
ServerPermission,
|
||||
User,
|
||||
} from '@prisma/client';
|
||||
import { ServerFolderPermission, ServerPermission, User } from '@prisma/client';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import passport from 'passport';
|
||||
|
||||
@@ -55,13 +50,6 @@ export const authenticate = (
|
||||
(permission: ServerPermission) => permission.serverId
|
||||
);
|
||||
|
||||
const serverCredentials = user.serverCredentials.map(
|
||||
(credential: ServerCredential) => ({
|
||||
id: credential.id,
|
||||
serverId: credential.serverId,
|
||||
})
|
||||
);
|
||||
|
||||
const props = {
|
||||
createdAt: user?.createdAt,
|
||||
enabled: user?.enabled,
|
||||
@@ -70,7 +58,6 @@ export const authenticate = (
|
||||
id: user?.id,
|
||||
isAdmin: user?.isAdmin,
|
||||
server: req.params.serverId,
|
||||
serverCredentials,
|
||||
serverFolderPermissions: user?.serverFolderPermissions,
|
||||
serverPermissions: user?.serverPermissions,
|
||||
updatedAt: user?.updatedAt,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { isJsonString } from '../utils';
|
||||
import { isJsonString } from '@utils/is-json-string';
|
||||
|
||||
export const errorHandler = (
|
||||
err: any,
|
||||
@@ -9,7 +9,7 @@ export const errorHandler = (
|
||||
) => {
|
||||
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, '');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
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 });
|
||||
|
||||
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 { controller } from '../controllers';
|
||||
import { controller } from '@controllers/index';
|
||||
import { validateRequest, validation } from '@validations/index';
|
||||
|
||||
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 { controller } from '../controllers';
|
||||
import { controller } from '@controllers/index';
|
||||
|
||||
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 passport from 'passport';
|
||||
import { controller } from '../controllers';
|
||||
import { authenticate } from '../middleware';
|
||||
import { controller } from '@controllers/index';
|
||||
import { authenticate } from '@middleware/authenticate';
|
||||
import { validation, validateRequest } from '@validations/index';
|
||||
|
||||
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('/refresh', controller.auth.refresh);
|
||||
router.post(
|
||||
'/refresh',
|
||||
validateRequest(validation.auth.refresh),
|
||||
controller.auth.refresh
|
||||
);
|
||||
|
||||
router.get('/ping', controller.auth.ping);
|
||||
|
||||
@@ -23,7 +23,21 @@ routes.use('/api/users', usersRouter);
|
||||
routes.use('/api/servers', serversRouter);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,87 @@
|
||||
import express, { Router } from 'express';
|
||||
import { controller } from '../controllers';
|
||||
import { authenticateAdmin } from '../middleware';
|
||||
import { controller } from '@controllers/index';
|
||||
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 });
|
||||
|
||||
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 { controller } from '../controllers';
|
||||
import { validation, validateRequest } from '@validations/index';
|
||||
|
||||
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 { 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 });
|
||||
|
||||
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 passport from 'passport';
|
||||
import 'express-async-errors';
|
||||
import { errorHandler } from './middleware';
|
||||
import { routes } from './routes';
|
||||
import { errorHandler } from '@/middleware';
|
||||
import { routes } from '@routes/index';
|
||||
|
||||
require('./lib/passport');
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
import { prisma } from '../lib';
|
||||
import { OffsetPagination, User } from '../types/types';
|
||||
import {
|
||||
ApiError,
|
||||
ApiSuccess,
|
||||
folderPermissions,
|
||||
splitNumberString,
|
||||
} from '../utils';
|
||||
import { OffsetPagination } from '@/types/types';
|
||||
import { ApiError } from '@/utils';
|
||||
import { prisma } from '@lib/prisma';
|
||||
import { folderPermissions } from '@utils/folder-permissions';
|
||||
|
||||
const findById = async (options: { id: number; user: User }) => {
|
||||
const findById = async (options: { id: string; user: User }) => {
|
||||
const { id, user } = options;
|
||||
const albumArtist = await prisma.albumArtist.findUnique({
|
||||
include: {
|
||||
@@ -32,7 +29,7 @@ const findById = async (options: { id: number; user: User }) => {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: albumArtist });
|
||||
return albumArtist;
|
||||
};
|
||||
|
||||
const findMany = async (
|
||||
@@ -40,37 +37,29 @@ const findMany = async (
|
||||
options: { serverFolderIds: string; user: User } & OffsetPagination
|
||||
) => {
|
||||
const { user, take, serverFolderIds: rServerFolderIds, skip } = options;
|
||||
const serverFolderIds = splitNumberString(rServerFolderIds);
|
||||
const serverFolderIds = rServerFolderIds.split(',');
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds!, user))) {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
||||
return {
|
||||
serverFolders: { some: { id: { equals: Number(serverFolderId) } } },
|
||||
};
|
||||
});
|
||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
|
||||
serverFolders: { some: { id: { equals: serverFolderId } } },
|
||||
}));
|
||||
|
||||
const totalEntries = await prisma.albumArtist.count({
|
||||
where: { OR: serverFoldersFilter },
|
||||
});
|
||||
const albumArtists = await prisma.albumArtist.findMany({
|
||||
include: { genres: true },
|
||||
skip,
|
||||
take,
|
||||
where: { OR: serverFoldersFilter },
|
||||
});
|
||||
|
||||
return ApiSuccess.ok({
|
||||
data: albumArtists,
|
||||
paginationItems: {
|
||||
const [totalEntries, albumArtists] = await prisma.$transaction([
|
||||
prisma.albumArtist.count({
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
prisma.albumArtist.findMany({
|
||||
include: { genres: true },
|
||||
skip,
|
||||
take,
|
||||
totalEntries,
|
||||
url: req.originalUrl,
|
||||
},
|
||||
});
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { data: albumArtists, totalEntries };
|
||||
};
|
||||
|
||||
export const albumArtistsService = {
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
import { Album } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
import { albumHelpers, AlbumSort } from '../helpers/albums.helpers';
|
||||
import { sharedHelpers } from '../helpers/shared.helpers';
|
||||
import { prisma } from '../lib';
|
||||
import { OffsetPagination, SortOrder, User } from '../types/types';
|
||||
import {
|
||||
ApiError,
|
||||
ApiSuccess,
|
||||
folderPermissions,
|
||||
getFolderPermissions,
|
||||
splitNumberString,
|
||||
} from '../utils';
|
||||
import { toRes } from './response';
|
||||
import { AuthUser } from '@/middleware';
|
||||
import { OffsetPagination, SortOrder } from '@/types/types';
|
||||
import { ApiError } from '@/utils';
|
||||
import { AlbumSort } from '@helpers/albums.helpers';
|
||||
import { helpers } from '@helpers/index';
|
||||
import { prisma } from '@lib/prisma';
|
||||
|
||||
const findById = async (options: {
|
||||
id: number;
|
||||
serverUrls?: string;
|
||||
user: User;
|
||||
}) => {
|
||||
const { id, user, serverUrls } = options;
|
||||
const findById = async (user: AuthUser, options: { id: string }) => {
|
||||
const { id } = options;
|
||||
|
||||
const album = await prisma.album.findUnique({
|
||||
include: {
|
||||
...albumHelpers.include({ serverUrls, songs: true }),
|
||||
serverFolders: true,
|
||||
},
|
||||
include: helpers.albums.include({ songs: true }),
|
||||
where: { id },
|
||||
});
|
||||
|
||||
@@ -32,105 +17,94 @@ const findById = async (options: {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
const serverFolderIds = album.serverFolders.map(
|
||||
(serverFolder) => serverFolder.id
|
||||
);
|
||||
const serverFolderId = album.serverFolders.map((s) => s.id);
|
||||
helpers.shared.checkServerFolderPermissions(user, { serverFolderId });
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds, user))) {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: toRes.albums([album], user)[0] });
|
||||
return album;
|
||||
};
|
||||
|
||||
const findMany = async (
|
||||
req: Request,
|
||||
options: {
|
||||
orderBy: SortOrder;
|
||||
serverFolderIds?: string;
|
||||
serverUrls?: string;
|
||||
sortBy: AlbumSort;
|
||||
user: User;
|
||||
} & OffsetPagination
|
||||
) => {
|
||||
const {
|
||||
user,
|
||||
take,
|
||||
serverFolderIds: rServerFolderIds,
|
||||
serverUrls,
|
||||
skip,
|
||||
sortBy,
|
||||
orderBy,
|
||||
} = options;
|
||||
export type AlbumFindManyOptions = {
|
||||
orderBy: SortOrder;
|
||||
serverFolderId?: string[];
|
||||
serverId: string;
|
||||
sortBy: AlbumSort;
|
||||
user: AuthUser;
|
||||
} & OffsetPagination;
|
||||
|
||||
const serverFolderIds = rServerFolderIds
|
||||
? splitNumberString(rServerFolderIds)
|
||||
: await getFolderPermissions(user);
|
||||
const findMany = async (options: AlbumFindManyOptions) => {
|
||||
const { take, serverFolderId, skip, sortBy, orderBy, user, serverId } =
|
||||
options;
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds!, user))) {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
const serverFoldersFilter = sharedHelpers.serverFolderFilter(
|
||||
serverFolderIds!
|
||||
);
|
||||
const serverFolderIds =
|
||||
serverFolderId ||
|
||||
(await helpers.shared.getAvailableServerFolderIds(user, { serverId }));
|
||||
|
||||
let totalEntries = 0;
|
||||
let albums: Album[];
|
||||
let albums;
|
||||
|
||||
if (sortBy === AlbumSort.RATING) {
|
||||
const [count, result] = await prisma.$transaction([
|
||||
prisma.albumRating.count({
|
||||
where: {
|
||||
album: { OR: serverFoldersFilter },
|
||||
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||
user: { id: user.id },
|
||||
},
|
||||
}),
|
||||
prisma.albumRating.findMany({
|
||||
include: {
|
||||
album: {
|
||||
include: { ...albumHelpers.include({ serverUrls, songs: false }) },
|
||||
include: helpers.albums.include({ songs: false, user }),
|
||||
},
|
||||
},
|
||||
orderBy: { value: orderBy },
|
||||
skip,
|
||||
take,
|
||||
where: {
|
||||
album: { OR: serverFoldersFilter },
|
||||
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||
user: { id: user.id },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
albums = result.map((rating) => rating.album) as Album[];
|
||||
albums = result.map((rating) => rating.album);
|
||||
totalEntries = count;
|
||||
} else {
|
||||
const [count, result] = await prisma.$transaction([
|
||||
} else if (sortBy === AlbumSort.FAVORITE) {
|
||||
[totalEntries, albums] = await prisma.$transaction([
|
||||
prisma.album.count({
|
||||
where: { OR: serverFoldersFilter },
|
||||
where: {
|
||||
AND: [
|
||||
helpers.shared.serverFolderFilter(serverFolderIds),
|
||||
{ favorites: { some: { userId: user.id } } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
prisma.album.findMany({
|
||||
include: { ...albumHelpers.include({ serverUrls, songs: false }) },
|
||||
orderBy: [{ ...albumHelpers.sort(sortBy, orderBy) }],
|
||||
include: helpers.albums.include({ songs: false, user }),
|
||||
skip,
|
||||
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({
|
||||
data: toRes.albums(albums, user),
|
||||
paginationItems: {
|
||||
skip,
|
||||
take,
|
||||
totalEntries,
|
||||
url: req.originalUrl,
|
||||
},
|
||||
});
|
||||
return { data: albums, totalEntries };
|
||||
};
|
||||
|
||||
export const albumsService = {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
import { prisma } from '../lib';
|
||||
import { OffsetPagination, User } from '../types/types';
|
||||
import {
|
||||
ApiError,
|
||||
ApiSuccess,
|
||||
folderPermissions,
|
||||
splitNumberString,
|
||||
} from '../utils';
|
||||
import { OffsetPagination } from '../types/types';
|
||||
import { ApiError, folderPermissions } from '../utils';
|
||||
|
||||
const findById = async (options: { id: number; user: User }) => {
|
||||
const findById = async (options: { id: string; user: User }) => {
|
||||
const { id, user } = options;
|
||||
|
||||
const artist = await prisma.artist.findUnique({
|
||||
@@ -16,19 +12,16 @@ const findById = async (options: { id: number; user: User }) => {
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!artist) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
if (!artist) throw ApiError.notFound('');
|
||||
|
||||
const serverFolderIds = artist.serverFolders.map(
|
||||
(serverFolder) => serverFolder.id
|
||||
);
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds, user))) {
|
||||
if (!(await folderPermissions(serverFolderIds, user)))
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: artist });
|
||||
return artist;
|
||||
};
|
||||
|
||||
const findMany = async (
|
||||
@@ -36,41 +29,28 @@ const findMany = async (
|
||||
options: { serverFolderIds: string; user: User } & OffsetPagination
|
||||
) => {
|
||||
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('');
|
||||
}
|
||||
|
||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
||||
return {
|
||||
serverFolders: {
|
||||
some: {
|
||||
id: { equals: Number(serverFolderId) },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
|
||||
serverFolders: { some: { id: { equals: serverFolderId } } },
|
||||
}));
|
||||
|
||||
const totalEntries = await prisma.artist.count({
|
||||
where: { OR: serverFoldersFilter },
|
||||
});
|
||||
const artists = await prisma.artist.findMany({
|
||||
include: { genres: true },
|
||||
skip,
|
||||
take,
|
||||
where: { OR: serverFoldersFilter },
|
||||
});
|
||||
|
||||
return ApiSuccess.ok({
|
||||
data: artists,
|
||||
paginationItems: {
|
||||
const [totalEntries, artists] = await prisma.$transaction([
|
||||
prisma.artist.count({
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
prisma.artist.findMany({
|
||||
include: { genres: true },
|
||||
skip,
|
||||
take,
|
||||
totalEntries,
|
||||
url: req.originalUrl,
|
||||
},
|
||||
});
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { data: artists, totalEntries };
|
||||
};
|
||||
|
||||
export const artistsService = {
|
||||
|
||||
@@ -8,22 +8,32 @@ import { ApiError } from '../utils/api-error';
|
||||
|
||||
const login = async (options: { username: string }) => {
|
||||
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) {
|
||||
const accessToken = generateToken(user.id);
|
||||
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 });
|
||||
if (!user) {
|
||||
throw ApiError.notFound('The user does not exist.');
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
@@ -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 }) => {
|
||||
@@ -59,19 +69,16 @@ const logout = async (options: { user: User }) => {
|
||||
const refresh = async (options: { refreshToken: string }) => {
|
||||
const { refreshToken } = options;
|
||||
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({
|
||||
where: { token: refreshToken },
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
throw ApiError.unauthorized('Invalid refresh token.');
|
||||
}
|
||||
if (!token) throw ApiError.unauthorized('Invalid refresh token.');
|
||||
|
||||
const newToken = generateToken(id);
|
||||
|
||||
return ApiSuccess.ok({ data: { accessToken: newToken } });
|
||||
return { accessToken: newToken };
|
||||
};
|
||||
|
||||
export const authService = {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
export * from './auth.service';
|
||||
export * from './servers.service';
|
||||
export * from './album-artists.service';
|
||||
export * from './users.service';
|
||||
export * from './artists.service';
|
||||
export * from './albums.service';
|
||||
import { albumArtistsService } from './album-artists.service';
|
||||
import { albumsService } from './albums.service';
|
||||
import { artistsService } from './artists.service';
|
||||
import { authService } from './auth.service';
|
||||
import { serversService } from './servers.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 {
|
||||
jellyfinApi,
|
||||
jellyfinTasks,
|
||||
subsonicApi,
|
||||
subsonicTasks,
|
||||
} from '../queue';
|
||||
import { User } from '../types/types';
|
||||
import { ApiError, ApiSuccess, splitNumberString } from '../utils';
|
||||
import { AuthUser } from '../middleware';
|
||||
import { subsonic } from '../queue';
|
||||
import { jellyfin } from '../queue/jellyfin';
|
||||
import { navidrome } from '../queue/navidrome';
|
||||
import { ApiError } 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;
|
||||
|
||||
helpers.shared.checkServerPermissions(user, { serverId: id });
|
||||
|
||||
const server = await prisma.server.findUnique({
|
||||
include: {
|
||||
serverFolders: user.isAdmin
|
||||
? true
|
||||
: {
|
||||
where: {
|
||||
OR: [
|
||||
{ isPublic: true },
|
||||
{ serverFolderPermissions: { some: { userId: user.id } } },
|
||||
],
|
||||
OR: [{ serverFolderPermissions: { some: { userId: user.id } } }],
|
||||
},
|
||||
},
|
||||
serverPermissions: {
|
||||
where: { userId: user.id },
|
||||
},
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
@@ -30,108 +101,162 @@ const findById = async (user: User, options: { id: number }) => {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
if (!user.isAdmin && server.serverFolders.length === 0) {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: server });
|
||||
return server;
|
||||
};
|
||||
|
||||
const findMany = async (user: User) => {
|
||||
let servers;
|
||||
|
||||
const findMany = async (user: AuthUser) => {
|
||||
if (user.isAdmin) {
|
||||
servers = await prisma.server.findMany({
|
||||
include: { serverFolders: true },
|
||||
});
|
||||
} else {
|
||||
servers = await prisma.server.findMany({
|
||||
return prisma.server.findMany({
|
||||
include: {
|
||||
serverFolders: {
|
||||
where: {
|
||||
OR: [
|
||||
{ isPublic: true },
|
||||
{ serverFolderPermissions: { some: { userId: user.id } } },
|
||||
],
|
||||
orderBy: { name: SortOrder.ASC },
|
||||
},
|
||||
serverPermissions: {
|
||||
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: {
|
||||
altToken?: string; // Used for Navidrome only
|
||||
name: string;
|
||||
remoteUserId: string;
|
||||
serverType: string;
|
||||
token: string;
|
||||
type: ServerType;
|
||||
url: string;
|
||||
username: string;
|
||||
}) => {
|
||||
const checkDuplicate = await prisma.server.findUnique({
|
||||
const isDuplicate = await prisma.server.findUnique({
|
||||
where: { url: options.url },
|
||||
});
|
||||
|
||||
if (checkDuplicate) {
|
||||
if (isDuplicate) {
|
||||
throw ApiError.conflict('Server already exists.');
|
||||
}
|
||||
|
||||
let musicFoldersData: {
|
||||
const serverFolders: {
|
||||
name: string;
|
||||
remoteId: string;
|
||||
serverId: number;
|
||||
serverId: string;
|
||||
}[] = [];
|
||||
|
||||
if (options.serverType === 'subsonic') {
|
||||
const musicFoldersRes = await subsonicApi.getMusicFolders({
|
||||
if (options.type === ServerType.SUBSONIC) {
|
||||
const serverFoldersRes = await subsonic.api.getMusicFolders({
|
||||
token: options.token,
|
||||
url: options.url,
|
||||
});
|
||||
|
||||
if (!musicFoldersRes) {
|
||||
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({
|
||||
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({
|
||||
data: {
|
||||
name: options.name,
|
||||
remoteUserId: options.remoteUserId,
|
||||
serverType: options.serverType,
|
||||
serverFolders: { create: serverFoldersCreate },
|
||||
serverUrls: { create: { url: options.url } },
|
||||
token: options.token,
|
||||
type: options.type,
|
||||
url: options.url,
|
||||
username: options.username,
|
||||
},
|
||||
});
|
||||
|
||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
||||
return {
|
||||
name: musicFolder.name,
|
||||
remoteId: String(musicFolder.id),
|
||||
serverId: server.id,
|
||||
};
|
||||
});
|
||||
|
||||
musicFoldersData.forEach(async (musicFolder) => {
|
||||
for (const serverFolder of serverFolders) {
|
||||
await prisma.serverFolder.upsert({
|
||||
create: musicFolder,
|
||||
update: { name: musicFolder.name },
|
||||
create: serverFolder,
|
||||
update: { name: serverFolder.name },
|
||||
where: {
|
||||
uniqueServerFolderId: {
|
||||
remoteId: musicFolder.remoteId,
|
||||
serverId: musicFolder.serverId,
|
||||
remoteId: serverFolder.remoteId,
|
||||
serverId: serverFolder.serverId,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: { ...server } });
|
||||
return server;
|
||||
}
|
||||
|
||||
if (options.serverType === 'jellyfin') {
|
||||
const musicFoldersRes = await jellyfinApi.getMusicFolders({
|
||||
if (options.type === ServerType.JELLYFIN) {
|
||||
const musicFoldersRes = await jellyfin.api.getMusicFolders({
|
||||
remoteUserId: options.remoteUserId,
|
||||
token: options.token,
|
||||
url: options.url,
|
||||
@@ -141,62 +266,72 @@ const create = async (options: {
|
||||
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({
|
||||
data: {
|
||||
name: options.name,
|
||||
remoteUserId: options.remoteUserId,
|
||||
serverType: options.serverType,
|
||||
serverFolders: { create: serverFoldersCreate },
|
||||
serverUrls: { create: { url: options.url } },
|
||||
token: options.token,
|
||||
type: options.type,
|
||||
url: options.url,
|
||||
username: options.username,
|
||||
},
|
||||
});
|
||||
|
||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
||||
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 server;
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: {} });
|
||||
throw ApiError.badRequest('Server type invalid.');
|
||||
};
|
||||
|
||||
const refresh = async (options: { id: number }) => {
|
||||
const { id } = options;
|
||||
const server = await prisma.server.findUnique({ where: { id } });
|
||||
const update = async (
|
||||
options: { id: string },
|
||||
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) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
let musicFoldersData: {
|
||||
let serverFolders: {
|
||||
name: string;
|
||||
remoteId: string;
|
||||
serverId: number;
|
||||
serverId: string;
|
||||
}[] = [];
|
||||
|
||||
if (server.serverType === 'subsonic') {
|
||||
const musicFoldersRes = await subsonicApi.getMusicFolders(server);
|
||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
||||
if (server.type === ServerType.SUBSONIC) {
|
||||
const serverFoldersRes = await subsonic.api.getMusicFolders(server);
|
||||
serverFolders = serverFoldersRes.map((musicFolder) => {
|
||||
return {
|
||||
name: musicFolder.name,
|
||||
remoteId: String(musicFolder.id),
|
||||
@@ -205,9 +340,9 @@ const refresh = async (options: { id: number }) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (server.serverType === 'jellyfin') {
|
||||
const musicFoldersRes = await jellyfinApi.getMusicFolders(server);
|
||||
musicFoldersData = musicFoldersRes.map((musicFolder) => {
|
||||
if (server.type === ServerType.JELLYFIN) {
|
||||
const musicFoldersRes = await jellyfin.api.getMusicFolders(server);
|
||||
serverFolders = musicFoldersRes.map((musicFolder) => {
|
||||
return {
|
||||
name: musicFolder.Name,
|
||||
remoteId: String(musicFolder.Id),
|
||||
@@ -217,29 +352,24 @@ const refresh = async (options: { id: number }) => {
|
||||
}
|
||||
|
||||
// mark as deleted if not found
|
||||
|
||||
musicFoldersData.forEach(async (musicFolder) => {
|
||||
for (const serverFolder of serverFolders) {
|
||||
await prisma.serverFolder.upsert({
|
||||
create: musicFolder,
|
||||
update: { name: musicFolder.name },
|
||||
create: serverFolder,
|
||||
update: { name: serverFolder.name },
|
||||
where: {
|
||||
uniqueServerFolderId: {
|
||||
remoteId: musicFolder.remoteId,
|
||||
serverId: musicFolder.serverId,
|
||||
remoteId: serverFolder.remoteId,
|
||||
serverId: serverFolder.serverId,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: { ...server } });
|
||||
return server;
|
||||
};
|
||||
|
||||
const fullScan = async (options: {
|
||||
id: number;
|
||||
serverFolderIds?: string;
|
||||
userId: number;
|
||||
}) => {
|
||||
const { id, serverFolderIds } = options;
|
||||
const fullScan = async (options: { id: string; serverFolderId?: string[] }) => {
|
||||
const { id, serverFolderId } = options;
|
||||
const server = await prisma.server.findUnique({
|
||||
include: { serverFolders: true },
|
||||
where: { id },
|
||||
@@ -250,52 +380,183 @@ const fullScan = async (options: {
|
||||
}
|
||||
|
||||
let serverFolders;
|
||||
if (serverFolderIds) {
|
||||
const selectedServerFolderIds = splitNumberString(serverFolderIds);
|
||||
serverFolders = server.serverFolders.filter((folder) =>
|
||||
selectedServerFolderIds?.includes(folder.id)
|
||||
if (serverFolderId) {
|
||||
serverFolders = server.serverFolders.filter((f) =>
|
||||
serverFolderId?.includes(f.id)
|
||||
);
|
||||
} else {
|
||||
serverFolders = server.serverFolders;
|
||||
}
|
||||
|
||||
if (server.serverType === 'jellyfin') {
|
||||
for (const serverFolder of serverFolders) {
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
completed: false,
|
||||
inProgress: true,
|
||||
name: 'Full scan',
|
||||
serverFolderId: serverFolder.id,
|
||||
},
|
||||
});
|
||||
|
||||
await jellyfinTasks.scanAll(server, serverFolder, task);
|
||||
}
|
||||
if (serverFolders.length === 0) {
|
||||
throw ApiError.notFound('No matching server folders found.');
|
||||
}
|
||||
|
||||
if (server.serverType === 'subsonic') {
|
||||
for (const serverFolder of serverFolders) {
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
completed: false,
|
||||
inProgress: true,
|
||||
name: 'Full scan',
|
||||
serverFolderId: serverFolder.id,
|
||||
},
|
||||
});
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
completed: false,
|
||||
name: 'Full scan',
|
||||
server: { connect: { id: server.id } },
|
||||
type: TaskType.FULL_SCAN,
|
||||
},
|
||||
});
|
||||
|
||||
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 = {
|
||||
create,
|
||||
createUrl,
|
||||
deleteById,
|
||||
deleteUrlById,
|
||||
disableUrlById,
|
||||
enableUrlById,
|
||||
findById,
|
||||
findMany,
|
||||
findServerUrlById,
|
||||
findUrlById,
|
||||
fullScan,
|
||||
refresh,
|
||||
remoteServerLogin,
|
||||
update,
|
||||
};
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
import { prisma } from '../lib';
|
||||
import { User } from '../types/types';
|
||||
import {
|
||||
ApiError,
|
||||
ApiSuccess,
|
||||
folderPermissions,
|
||||
splitNumberString,
|
||||
} from '../utils';
|
||||
import { toRes } from './response';
|
||||
import { SortOrder } from '../types/types';
|
||||
import { ApiError, ApiSuccess, folderPermissions } from '../utils';
|
||||
// import { toRes } from './response';
|
||||
import { SongRequestParams } from './types';
|
||||
|
||||
const findById = async (options: { id: number; user: User }) => {
|
||||
const { id, user } = options;
|
||||
const findById = async (options: { id: string; user: User }) => {
|
||||
const { id } = options;
|
||||
|
||||
const album = await prisma.album.findUnique({
|
||||
include: {
|
||||
_count: true,
|
||||
albumArtist: true,
|
||||
albumArtists: true,
|
||||
genres: true,
|
||||
songs: {
|
||||
include: {
|
||||
@@ -26,15 +22,16 @@ const findById = async (options: { id: number; user: User }) => {
|
||||
genres: true,
|
||||
images: true,
|
||||
},
|
||||
orderBy: [{ disc: 'asc' }, { track: 'asc' }],
|
||||
orderBy: [
|
||||
{ discNumber: SortOrder.ASC },
|
||||
{ trackNumber: SortOrder.ASC },
|
||||
],
|
||||
},
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!album) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
if (!album) throw ApiError.notFound('');
|
||||
|
||||
// if (!(await folderPermissions([album?.serverFolderId], user))) {
|
||||
// throw ApiError.forbidden('');
|
||||
@@ -49,37 +46,37 @@ const findMany = async (
|
||||
) => {
|
||||
const {
|
||||
albumIds: rawAlbumIds,
|
||||
artistIds: rawArtistIds,
|
||||
// artistIds: rawArtistIds,
|
||||
serverId,
|
||||
songIds: rawSongIds,
|
||||
user,
|
||||
skip,
|
||||
take,
|
||||
serverFolderIds: rServerFolderIds,
|
||||
} = options;
|
||||
const serverFolderIds = splitNumberString(rServerFolderIds);
|
||||
const albumIds = splitNumberString(rawAlbumIds);
|
||||
const artistIds = splitNumberString(rawArtistIds);
|
||||
const songIds = splitNumberString(rawSongIds);
|
||||
const serverFolderIds = rServerFolderIds.split(',');
|
||||
const albumIds = rawAlbumIds && rawAlbumIds.split(',');
|
||||
// const artistIds = rawArtistIds && rawArtistIds.split(',');
|
||||
const songIds = rawSongIds && rawSongIds.split(',');
|
||||
|
||||
if (serverFolderIds) {
|
||||
if (!(await folderPermissions(serverFolderIds, user))) {
|
||||
if (!(await folderPermissions(serverFolderIds, user)))
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
}
|
||||
|
||||
// const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
||||
// return { serverFolders: { id: { equals: serverFolderId } } };
|
||||
// });
|
||||
|
||||
const serverFoldersFilter = {
|
||||
serverFolders: { some: { id: { in: serverFolderIds } } },
|
||||
};
|
||||
// const serverFoldersFilter = {
|
||||
// serverFolders: { some: { id: { in: serverFolderIds } } },
|
||||
// };
|
||||
|
||||
const [totalEntries, songs] = await prisma.$transaction([
|
||||
prisma.song.count({
|
||||
where: {
|
||||
OR: [
|
||||
serverFoldersFilter,
|
||||
// serverFoldersFilter,
|
||||
{
|
||||
albumId: { in: albumIds },
|
||||
id: { in: songIds },
|
||||
@@ -96,19 +93,16 @@ const findMany = async (
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
where: { OR: serverFoldersFilter },
|
||||
where: {
|
||||
AND: {
|
||||
// OR: serverFoldersFilter,
|
||||
serverId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return ApiSuccess.ok({
|
||||
data: songs,
|
||||
paginationItems: {
|
||||
skip,
|
||||
take,
|
||||
totalEntries,
|
||||
url: req.originalUrl,
|
||||
},
|
||||
});
|
||||
return { data: songs, totalEntries };
|
||||
};
|
||||
|
||||
export const songsService = {
|
||||
|
||||
@@ -4,5 +4,6 @@ export interface SongRequestParams extends OffsetPagination {
|
||||
albumIds?: string;
|
||||
artistIds?: string;
|
||||
serverFolderIds: string;
|
||||
serverId: string;
|
||||
songIds?: string;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,32 @@
|
||||
import { prisma, exclude } from '../lib';
|
||||
import { ApiError, ApiSuccess } from '../utils';
|
||||
import { prisma } from '../lib';
|
||||
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 user = await prisma.user.findUnique({
|
||||
include: {
|
||||
serverFolderPermissions: true,
|
||||
},
|
||||
|
||||
if (!user.isAdmin && user.id !== id) {
|
||||
throw ApiError.forbidden();
|
||||
}
|
||||
|
||||
const uniqueUser = await prisma.user.findUnique({
|
||||
include: { serverFolderPermissions: true },
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!uniqueUser) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
return ApiSuccess.ok({ data: exclude(user, 'password') });
|
||||
return uniqueUser;
|
||||
};
|
||||
|
||||
const getMany = async () => {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
createdAt: true,
|
||||
enabled: true,
|
||||
id: true,
|
||||
isAdmin: true,
|
||||
serverFolderPermissions: true,
|
||||
updatedAt: true,
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
|
||||
return ApiSuccess.ok({ data: users });
|
||||
const findMany = async () => {
|
||||
const users = await prisma.user.findMany({});
|
||||
return users;
|
||||
};
|
||||
|
||||
export const usersService = {
|
||||
getMany,
|
||||
getOne,
|
||||
findById,
|
||||
findMany,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { prisma } from '../lib';
|
||||
import { User } from '../types/types';
|
||||
|
||||
export enum Roles {
|
||||
NONE = 0,
|
||||
@@ -24,12 +24,8 @@ export const folderPermissions = async (serverFolderIds: any[], user: User) => {
|
||||
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{ isPublic: false },
|
||||
{
|
||||
serverFolderPermissions: {
|
||||
some: { userId: { equals: user.id } },
|
||||
@@ -66,12 +62,8 @@ export const getFolderPermissions = async (user: User) => {
|
||||
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
{ isPublic: false },
|
||||
{
|
||||
serverFolderPermissions: {
|
||||
some: { userId: { equals: user.id } },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { paginationValidation, idValidation } from './shared.validation';
|
||||
import { idValidation, paginationValidation } from './shared.validation';
|
||||
|
||||
export const list = {
|
||||
body: z.object({}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { AlbumSort } from '../helpers/albums.helpers';
|
||||
import { AlbumSort } from '@helpers/albums.helpers';
|
||||
import {
|
||||
idValidation,
|
||||
orderByValidation,
|
||||
@@ -22,13 +22,32 @@ const list = {
|
||||
|
||||
const detail = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('id') }),
|
||||
params: z.object({
|
||||
...idValidation('albumId'),
|
||||
...idValidation('serverId'),
|
||||
}),
|
||||
query: z.object({
|
||||
...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 = {
|
||||
detail,
|
||||
detailSongList,
|
||||
list,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { AlbumSort } from '../helpers/albums.helpers';
|
||||
import { AlbumSort } from '@helpers/albums.helpers';
|
||||
import {
|
||||
idValidation,
|
||||
orderByValidation,
|
||||
|
||||
@@ -4,7 +4,32 @@ import { idValidation } from './shared.validation';
|
||||
|
||||
const detail = {
|
||||
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({}),
|
||||
};
|
||||
|
||||
@@ -27,26 +52,103 @@ const create = {
|
||||
|
||||
const scan = {
|
||||
body: z.object({ serverFolderId: z.string().array().optional() }),
|
||||
params: z.object({ ...idValidation('id') }),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const refresh = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('id') }),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const createCredential = {
|
||||
body: z.object({ credential: z.string() }),
|
||||
params: z.object({ ...idValidation('id') }),
|
||||
body: z.object({ credential: z.string(), username: z.string() }),
|
||||
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({}),
|
||||
};
|
||||
|
||||
export const serversValidation = {
|
||||
create,
|
||||
createCredential,
|
||||
createUrl,
|
||||
deleteCredential,
|
||||
deleteServer,
|
||||
deleteUrl,
|
||||
detail,
|
||||
disableCredential,
|
||||
disableUrl,
|
||||
enableCredential,
|
||||
enableUrl,
|
||||
getCredentialDetail,
|
||||
list,
|
||||
refresh,
|
||||
scan,
|
||||
update,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Modified from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware
|
||||
import { Request, RequestHandler } from 'express';
|
||||
import { z, ZodError, ZodSchema } from 'zod';
|
||||
import { SortOrder } from '../types/types';
|
||||
import { ApiError } from '../utils';
|
||||
import { SortOrder } from '@/types/types';
|
||||
import { ApiError } from '@/utils';
|
||||
// Modified from zod-express-middleware: https://github.com/Aquila169/zod-express-middleware
|
||||
|
||||
export type TypedRequest<
|
||||
S extends {
|
||||
|
||||
Reference in New Issue
Block a user