Redo server functionality

This commit is contained in:
jeffvli
2022-10-24 21:41:47 -07:00
parent db8a7d6a63
commit 4a3ce02805
40 changed files with 1986 additions and 952 deletions
@@ -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,
};
+88 -46
View File
@@ -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,
};
+41 -32
View File
@@ -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,
};
+31 -29
View File
@@ -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 };
+17 -6
View File
@@ -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,
};
+151 -46
View File
@@ -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,
};
+24 -26
View File
@@ -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,
};
+13 -12
View File
@@ -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,
};
+41 -27
View File
@@ -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:
+520
View File
@@ -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,
};
+9
View File
@@ -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,
};
+102 -5
View File
@@ -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,
};
+28 -3
View File
@@ -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 -14
View File
@@ -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,
+2 -2
View File
@@ -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, '');
});
+8 -3
View File
@@ -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
);
+18 -3
View File
@@ -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
);
+3 -3
View File
@@ -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);
+19 -5
View File
@@ -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);
+15 -1
View File
@@ -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();
});
+78 -8
View File
@@ -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
);
+7 -2
View File
@@ -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));
});
+9 -3
View File
@@ -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
);
+2 -2
View File
@@ -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');
+22 -33
View File
@@ -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 = {
+59 -85
View File
@@ -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 = {
+23 -43
View File
@@ -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 = {
+27 -20
View File
@@ -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 = {
+15 -6
View File
@@ -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,
};
-240
View File
@@ -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,
};
+399 -138
View File
@@ -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,
};
+30 -36
View File
@@ -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 = {
+1
View File
@@ -4,5 +4,6 @@ export interface SongRequestParams extends OffsetPagination {
albumIds?: string;
artistIds?: string;
serverFolderIds: string;
serverId: string;
songIds?: string;
}
+18 -25
View File
@@ -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 -9
View File
@@ -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({}),
+21 -2
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { AlbumSort } from '../helpers/albums.helpers';
import { AlbumSort } from '@helpers/albums.helpers';
import {
idValidation,
orderByValidation,
+107 -5
View File
@@ -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,
};
+3 -3
View File
@@ -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 {