Rename files, add utils

This commit is contained in:
jeffvli
2022-07-24 00:26:03 -07:00
parent 650deacbdd
commit c0ea48be78
34 changed files with 823 additions and 244 deletions

View File

@@ -17,7 +17,7 @@ const getAlbumArtists = async (req: Request, res: Response) => {
});
const { limit, page, serverFolderIds } = req.query;
const data = await albumArtistsService.getMany(req, {
const data = await albumArtistsService.findMany(req, {
limit: Number(limit),
page: Number(page),
serverFolderIds: String(serverFolderIds),
@@ -27,15 +27,15 @@ const getAlbumArtists = async (req: Request, res: Response) => {
return res.status(data.statusCode).json(getSuccessResponse(data));
};
const getAlbumArtist = async (req: Request, res: Response) => {
const getAlbumArtistById = async (req: Request, res: Response) => {
validateRequest(req, { params: z.object({ ...idValidation }) });
const { id } = req.params;
const data = await albumArtistsService.getOne({
const data = await albumArtistsService.findById({
id: Number(id),
user: req.auth,
});
return res.status(data.statusCode).json(getSuccessResponse(data));
};
export const albumArtistsController = { getAlbumArtist, getAlbumArtists };
export const albumArtistsController = { getAlbumArtistById, getAlbumArtists };

View File

@@ -8,14 +8,20 @@ import {
validateRequest,
} from '../utils';
const getAlbum = async (req: Request, res: Response) => {
validateRequest(req, { params: z.object({ ...idValidation }) });
const getAlbumById = async (req: Request, res: Response) => {
validateRequest(req, {
params: z.object({ ...idValidation }),
query: z.object({ serverUrls: z.optional(z.string().min(1)) }),
});
const { id } = req.params;
const data = await albumsService.getOne({
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));
};
@@ -23,15 +29,17 @@ const getAlbums = async (req: Request, res: Response) => {
validateRequest(req, {
query: z.object({
...paginationValidation,
serverFolderIds: z.string().min(1),
serverFolderIds: z.optional(z.string().min(1)),
serverUrls: z.optional(z.string().min(1)),
}),
});
const { limit, page, serverFolderIds } = req.query;
const data = await albumsService.getMany(req, {
const { limit, page, serverFolderIds, serverUrls } = req.query;
const data = await albumsService.findMany(req, {
limit: Number(limit),
page: Number(page),
serverFolderIds: String(serverFolderIds),
serverFolderIds: serverFolderIds && String(serverFolderIds),
serverUrls: serverUrls && String(serverUrls),
user: req.auth,
});
@@ -39,6 +47,6 @@ const getAlbums = async (req: Request, res: Response) => {
};
export const albumsController = {
getAlbum,
getAlbumById,
getAlbums,
};

View File

@@ -8,11 +8,11 @@ import {
validateRequest,
} from '../utils';
const getArtist = async (req: Request, res: Response) => {
const getArtistById = async (req: Request, res: Response) => {
validateRequest(req, { params: z.object({ ...idValidation }) });
const { id } = req.params;
const data = await artistsService.getOne({
const data = await artistsService.findById({
id: Number(id),
user: req.auth,
});
@@ -28,7 +28,7 @@ const getArtists = async (req: Request, res: Response) => {
});
const { limit, page, serverFolderIds } = req.query;
const data = await artistsService.getMany(req, {
const data = await artistsService.findMany(req, {
limit: Number(limit),
page: Number(page),
serverFolderIds: String(serverFolderIds),
@@ -38,4 +38,4 @@ const getArtists = async (req: Request, res: Response) => {
return res.status(data.statusCode).json(getSuccessResponse(data));
};
export const artistsController = { getArtist, getArtists };
export const artistsController = { getArtistById, getArtists };

View File

@@ -1,6 +1,6 @@
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';
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';

View File

@@ -1,13 +1,14 @@
import { Request, Response } from 'express';
import { z } from 'zod';
import { prisma } from '../lib';
import { serversService } from '../services';
import { getSuccessResponse, idValidation, validateRequest } from '../utils';
const getServer = async (req: Request, res: Response) => {
const getServerById = async (req: Request, res: Response) => {
validateRequest(req, { params: z.object({ ...idValidation }) });
const { id } = req.params;
const data = await serversService.getOne(req.auth, {
const data = await serversService.findById(req.auth, {
id: Number(id),
});
@@ -15,22 +16,13 @@ const getServer = async (req: Request, res: Response) => {
};
const getServers = async (req: Request, res: Response) => {
const data = await serversService.getMany(req.auth);
const data = await serversService.findMany(req.auth);
return res.status(data.statusCode).json(getSuccessResponse(data));
};
const createServer = async (req: Request, res: Response) => {
const { name, url, username, remoteUserId, token, serverType } = req.body;
const data = await serversService.create({
name,
remoteUserId,
serverType,
token,
url,
username,
});
const data = await serversService.create(req.body);
return res.status(data.statusCode).json(getSuccessResponse(data));
};
@@ -59,9 +51,21 @@ const scanServer = async (req: Request, res: Response) => {
return res.status(data.statusCode).json(getSuccessResponse(data));
};
const getFolder = async (req: Request, res: Response) => {
const data = await prisma.folder.findUnique({
include: {
children: true,
},
where: { id: Number(req.params.id) },
});
return res.status(200).json(getSuccessResponse({ data, statusCode: 200 }));
};
export const serversController = {
createServer,
getServer,
getFolder,
getServerById,
getServers,
refreshServer,
scanServer,

View File

@@ -0,0 +1,35 @@
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 { limit, page, serverFolderIds } = req.query;
const data = await songsService.findMany(req, {
limit: Number(limit),
page: Number(page),
serverFolderIds: String(serverFolderIds),
user: req.auth,
});
return res.status(data.statusCode).json(getSuccessResponse(data));
};
export const songsController = {
getSongs,
};

View File

@@ -13,5 +13,5 @@ albumArtistsRouter.get(
albumArtistsRouter.get(
'/:id',
authenticateLocal,
albumArtistsController.getAlbumArtist
albumArtistsController.getAlbumArtistById
);

View File

@@ -6,4 +6,4 @@ export const albumsRouter: Router = express.Router();
albumsRouter.get('/', authenticateLocal, albumsController.getAlbums);
albumsRouter.get('/:id', authenticateLocal, albumsController.getAlbum);
albumsRouter.get('/:id', authenticateLocal, albumsController.getAlbumById);

View File

@@ -6,4 +6,4 @@ export const artistsRouter: Router = express.Router();
artistsRouter.get('/', authenticateLocal, artistsController.getArtists);
artistsRouter.get('/:id', authenticateLocal, artistsController.getArtist);
artistsRouter.get('/:id', authenticateLocal, artistsController.getArtistById);

View File

@@ -1,11 +1,12 @@
import { Router } from 'express';
import { albumArtistsRouter } from './album-artists-route';
import { albumsRouter } from './albums-route';
import { artistsRouter } from './artists-route';
import { authRouter } from './auth-route';
import { serversRouter } from './servers-route';
import { tasksRouter } from './tasks-route';
import { usersRouter } from './users-route';
import { albumArtistsRouter } from './album-artists.route';
import { albumsRouter } from './albums.route';
import { artistsRouter } from './artists.route';
import { authRouter } from './auth.route';
import { serversRouter } from './servers.route';
import { songsRouter } from './songs.route';
import { tasksRouter } from './tasks.route';
import { usersRouter } from './users.route';
export const routes = Router();
@@ -16,3 +17,4 @@ routes.use('/api/users', usersRouter);
routes.use('/api/album-artists', albumArtistsRouter);
routes.use('/api/artists', artistsRouter);
routes.use('/api/albums', albumsRouter);
routes.use('/api/songs', songsRouter);

View File

@@ -6,7 +6,7 @@ export const serversRouter: Router = express.Router();
serversRouter.get('/', authenticateLocal, serversController.getServers);
serversRouter.get('/:id', authenticateLocal, serversController.getServer);
serversRouter.get('/:id', authenticateLocal, serversController.getServerById);
serversRouter.get(
'/:id/refresh',
@@ -14,6 +14,12 @@ serversRouter.get(
serversController.refreshServer
);
serversRouter.get(
'/:id/folder',
authenticateAdmin,
serversController.getFolder
);
serversRouter.post('/', authenticateAdmin, serversController.createServer);
serversRouter.post(

View File

@@ -0,0 +1,7 @@
import express, { Router } from 'express';
import { songsController } from '../controllers/songs.controller';
import { authenticateLocal } from '../middleware';
export const songsRouter: Router = express.Router();
songsRouter.get('/', authenticateLocal, songsController.getSongs);

View File

@@ -4,46 +4,51 @@ import { OffsetPagination, User } from '../types/types';
import {
ApiError,
ApiSuccess,
hasFolderAccess,
folderPermissions,
splitNumberString,
} from '../utils';
const getOne = async (options: { id: number; user: User }) => {
const findById = async (options: { id: number; user: User }) => {
const { id, user } = options;
const album = await prisma.albumArtist.findUnique({
const albumArtist = await prisma.albumArtist.findUnique({
include: {
albums: { include: { songs: true } },
genres: true,
images: true,
serverFolders: true,
},
where: { id },
});
if (!album) {
if (!albumArtist) {
throw ApiError.notFound('');
}
if (!(await hasFolderAccess([album?.serverFolderId], user))) {
const serverFolderIds = albumArtist.serverFolders.map(
(serverFolder) => serverFolder.id
);
if (!(await folderPermissions(serverFolderIds, user))) {
throw ApiError.forbidden('');
}
return ApiSuccess.ok({ data: album });
return ApiSuccess.ok({ data: albumArtist });
};
const getMany = async (
const findMany = async (
req: Request,
options: { serverFolderIds: string; user: User } & OffsetPagination
) => {
const { user, limit, page, serverFolderIds: rServerFolderIds } = options;
const serverFolderIds = splitNumberString(rServerFolderIds);
if (!(await hasFolderAccess(serverFolderIds, user))) {
if (!(await folderPermissions(serverFolderIds!, user))) {
throw ApiError.forbidden('');
}
const serverFoldersFilter = serverFolderIds.map((serverFolderId: number) => {
const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
return {
serverFolder: { id: { equals: Number(serverFolderId) } },
serverFolders: { some: { id: { equals: Number(serverFolderId) } } },
};
});
@@ -62,6 +67,7 @@ const getMany = async (
data: albumArtists,
paginationItems: {
limit,
page,
startIndex,
totalEntries,
url: req.originalUrl,
@@ -70,6 +76,6 @@ const getMany = async (
};
export const albumArtistsService = {
getMany,
getOne,
findById,
findMany,
};

View File

@@ -1,92 +0,0 @@
import { Request } from 'express';
import { prisma } from '../lib';
import { OffsetPagination, User } from '../types/types';
import {
ApiError,
ApiSuccess,
hasFolderAccess,
splitNumberString,
} from '../utils';
const getOne = async (options: { id: number; user: User }) => {
const { id, user } = options;
const album = await prisma.album.findUnique({
include: {
_count: true,
albumArtist: true,
genres: true,
songs: {
include: {
album: true,
artists: true,
externals: true,
genres: true,
images: true,
},
orderBy: [{ disc: 'asc' }, { track: 'asc' }],
},
},
where: { id },
});
if (!album) {
throw ApiError.notFound('');
}
if (!(await hasFolderAccess([album?.serverFolderId], user))) {
throw ApiError.forbidden('');
}
return ApiSuccess.ok({ data: album });
};
const getMany = async (
req: Request,
options: { serverFolderIds: string; user: User } & OffsetPagination
) => {
const { user, limit, page, serverFolderIds: rServerFolderIds } = options;
const serverFolderIds = splitNumberString(rServerFolderIds);
if (!(await hasFolderAccess(serverFolderIds, user))) {
throw ApiError.forbidden('');
}
const serverFoldersFilter = serverFolderIds.map((serverFolderId: number) => {
return {
ServerFolder: {
id: { equals: Number(serverFolderId) },
},
};
});
const startIndex = limit * page;
const totalEntries = await prisma.album.count({
where: { OR: serverFoldersFilter },
});
const albums = await prisma.album.findMany({
include: {
_count: { select: { songs: true } },
albumArtist: true,
genres: true,
},
skip: startIndex,
take: limit,
where: { OR: serverFoldersFilter },
});
return ApiSuccess.ok({
data: albums,
paginationItems: {
limit,
startIndex,
totalEntries,
url: req.originalUrl,
},
});
};
export const albumsService = {
getMany,
getOne,
};

View File

@@ -0,0 +1,148 @@
import { Request } from 'express';
import { prisma } from '../lib';
import { OffsetPagination, User } from '../types/types';
import {
ApiError,
ApiSuccess,
folderPermissions,
getFolderPermissions,
splitNumberString,
} from '../utils';
import { toRes } from './response';
const findById = async (options: {
id: number;
serverUrls?: string;
user: User;
}) => {
const { id, user, serverUrls } = options;
const album = await prisma.album.findUnique({
include: {
_count: true,
albumArtist: true,
genres: true,
images: true,
server: {
include: {
serverUrls: serverUrls
? { where: { id: { in: splitNumberString(serverUrls) } } }
: true,
},
},
serverFolders: true,
songs: {
include: {
album: true,
artists: true,
externals: true,
genres: true,
images: true,
},
orderBy: [{ disc: 'asc' }, { track: 'asc' }],
},
},
where: { id },
});
if (!album) {
throw ApiError.notFound('');
}
const serverFolderIds = album.serverFolders.map(
(serverFolder) => serverFolder.id
);
if (!(await folderPermissions(serverFolderIds, user))) {
throw ApiError.forbidden('');
}
return ApiSuccess.ok({ data: toRes.albums([album], user)[0] });
};
const findMany = async (
req: Request,
options: {
serverFolderIds?: string;
serverUrls?: string;
user: User;
} & OffsetPagination
) => {
const {
user,
limit,
page,
serverFolderIds: rServerFolderIds,
serverUrls,
} = options;
const serverFolderIds = rServerFolderIds
? splitNumberString(rServerFolderIds)
: await getFolderPermissions(user);
if (!(await folderPermissions(serverFolderIds!, user))) {
throw ApiError.forbidden('');
}
const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
return {
serverFolders: {
some: {
id: { equals: Number(serverFolderId) },
},
},
};
});
const startIndex = limit * page;
const totalEntries = await prisma.album.count({
where: {
OR: [...serverFoldersFilter],
},
});
const albums = await prisma.album.findMany({
include: {
_count: { select: { favorites: true, songs: true } },
albumArtist: true,
genres: true,
images: true,
server: {
include: {
serverUrls: serverUrls
? { where: { id: { in: splitNumberString(serverUrls) } } }
: true,
},
},
songs: {
include: {
album: true,
artists: true,
externals: true,
genres: true,
images: true,
},
orderBy: [{ disc: 'asc' }, { track: 'asc' }],
},
},
skip: startIndex,
take: limit,
where: { OR: serverFoldersFilter },
});
return ApiSuccess.ok({
data: toRes.albums(albums, user),
paginationItems: {
limit,
page,
startIndex,
totalEntries,
url: req.originalUrl,
},
});
};
export const albumsService = {
findById,
findMany,
};

View File

@@ -4,44 +4,50 @@ import { OffsetPagination, User } from '../types/types';
import {
ApiError,
ApiSuccess,
hasFolderAccess,
folderPermissions,
splitNumberString,
} from '../utils';
const getOne = async (options: { id: number; user: User }) => {
const findById = async (options: { id: number; user: User }) => {
const { id, user } = options;
const album = await prisma.artist.findUnique({
include: { genres: true },
const artist = await prisma.artist.findUnique({
include: { genres: true, serverFolders: true },
where: { id },
});
if (!album) {
if (!artist) {
throw ApiError.notFound('');
}
if (!(await hasFolderAccess([album?.serverFolderId], user))) {
const serverFolderIds = artist.serverFolders.map(
(serverFolder) => serverFolder.id
);
if (!(await folderPermissions(serverFolderIds, user))) {
throw ApiError.forbidden('');
}
return ApiSuccess.ok({ data: album });
return ApiSuccess.ok({ data: artist });
};
const getMany = async (
const findMany = async (
req: Request,
options: { serverFolderIds: string; user: User } & OffsetPagination
) => {
const { user, limit, page, serverFolderIds: rServerFolderIds } = options;
const serverFolderIds = splitNumberString(rServerFolderIds);
if (!(await hasFolderAccess(serverFolderIds, user))) {
if (!(await folderPermissions(serverFolderIds!, user))) {
throw ApiError.forbidden('');
}
const serverFoldersFilter = serverFolderIds.map((serverFolderId: number) => {
const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
return {
ServerFolder: {
id: { equals: Number(serverFolderId) },
serverFolders: {
some: {
id: { equals: Number(serverFolderId) },
},
},
};
});
@@ -61,6 +67,7 @@ const getMany = async (
data: artists,
paginationItems: {
limit,
page,
startIndex,
totalEntries,
url: req.originalUrl,
@@ -69,6 +76,6 @@ const getMany = async (
};
export const artistsService = {
getMany,
getOne,
findById,
findMany,
};

View File

@@ -1,6 +1,6 @@
import bcrypt from 'bcryptjs';
import { prisma } from '../lib';
import { ApiSuccess } from '../utils';
import { ApiSuccess, randomString } from '../utils';
import { ApiError } from '../utils/api-error';
const login = async (options: { username: string }) => {
@@ -21,6 +21,7 @@ const register = async (options: { password: string; username: string }) => {
const hashedPassword = await bcrypt.hash(password, 12);
const user = await prisma.user.create({
data: {
deviceId: `${username}_${randomString(10)}`,
enabled: false,
password: hashedPassword,
username,

View File

@@ -1,6 +1,6 @@
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';
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';

View File

@@ -0,0 +1,197 @@
/* eslint-disable no-underscore-dangle */
import { 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 relatedArtists = (items: any[]) => {
return (
items?.map((item: any) => {
return {
deleted: item.deleted,
id: item.id,
name: item.name,
remoteId: item.remoteId,
};
}) || []
);
};
const relatedGenres = (genres: any[]) => {
return (
genres?.map((genre) => {
return {
id: genre.id,
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;
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 {
albumId: item.albumId,
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,
image: primaryImage(item.images, serverType, url, item.remoteId),
name: item.name,
remoteCreatedAt: item.remoteCreatedAt,
remoteId: item.remoteId,
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];
return {
albumArtistId: item.albumArtistId,
createdAt: item.createdAt,
dateCreated: item.date,
deleted: item.deleted,
genres: relatedGenres(item.genres),
id: item.id,
image: primaryImage(item.images, serverType, url, item.remoteId),
name: item.name,
remoteCreatedAt: item.remoteCreatedAt,
remoteId: item.remoteId,
serverFolderId: item.serverFolderId,
serverType,
songCount: item._count.songs,
songs: songs(item.songs, {
deviceId: user.deviceId,
serverType,
token,
url,
userId: remoteUserId,
}),
updatedAt: item.updatedAt,
year: item.year,
};
}) || []
);
};
export const toRes = {
albums,
songs,
};

View File

@@ -8,11 +8,11 @@ import {
import { User } from '../types/types';
import { ApiError, ApiSuccess, splitNumberString } from '../utils';
const getOne = async (user: User, options: { id: number }) => {
const findById = async (user: User, options: { id: number }) => {
const { id } = options;
const server = await prisma.server.findUnique({
include: {
serverFolder: user.isAdmin
serverFolders: user.isAdmin
? true
: {
where: {
@@ -30,24 +30,24 @@ const getOne = async (user: User, options: { id: number }) => {
throw ApiError.notFound('');
}
if (!user.isAdmin && server.serverFolder.length === 0) {
if (!user.isAdmin && server.serverFolders.length === 0) {
throw ApiError.forbidden('');
}
return ApiSuccess.ok({ data: server });
};
const getMany = async (user: User) => {
const findMany = async (user: User) => {
let servers;
if (user.isAdmin) {
servers = await prisma.server.findMany({
include: { serverFolder: true },
include: { serverFolders: true },
});
} else {
servers = await prisma.server.findMany({
include: {
serverFolder: {
serverFolders: {
where: {
OR: [
{ isPublic: true },
@@ -56,7 +56,7 @@ const getMany = async (user: User) => {
},
},
},
where: { serverFolder: { some: { isPublic: true } } },
where: { serverFolders: { some: { isPublic: true } } },
});
}
@@ -71,7 +71,13 @@ const create = async (options: {
url: string;
username: string;
}) => {
const server = await prisma.server.create({ data: options });
const checkDuplicate = await prisma.server.findUnique({
where: { url: options.url },
});
if (checkDuplicate) {
throw ApiError.conflict('Server already exists.');
}
let musicFoldersData: {
name: string;
@@ -80,7 +86,26 @@ const create = async (options: {
}[] = [];
if (options.serverType === 'subsonic') {
const musicFoldersRes = await subsonicApi.getMusicFolders(server);
const musicFoldersRes = await subsonicApi.getMusicFolders({
token: options.token,
url: options.url,
});
if (!musicFoldersRes) {
throw ApiError.badRequest('Server is inaccessible.');
}
const server = await prisma.server.create({
data: {
name: options.name,
remoteUserId: options.remoteUserId,
serverType: options.serverType,
token: options.token,
url: options.url,
username: options.username,
},
});
musicFoldersData = musicFoldersRes.map((musicFolder) => {
return {
name: musicFolder.name,
@@ -88,10 +113,46 @@ const create = async (options: {
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 } });
}
if (options.serverType === 'jellyfin') {
const musicFoldersRes = await jellyfinApi.getMusicFolders(server);
const musicFoldersRes = await jellyfinApi.getMusicFolders({
remoteUserId: options.remoteUserId,
token: options.token,
url: options.url,
});
if (!musicFoldersRes) {
throw ApiError.badRequest('Server is inaccessible.');
}
const server = await prisma.server.create({
data: {
name: options.name,
remoteUserId: options.remoteUserId,
serverType: options.serverType,
serverUrls: { create: { url: options.url } },
token: options.token,
url: options.url,
username: options.username,
},
});
musicFoldersData = musicFoldersRes.map((musicFolder) => {
return {
name: musicFolder.Name,
@@ -99,22 +160,24 @@ const create = async (options: {
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 } });
}
musicFoldersData.forEach(async (musicFolder) => {
await prisma.serverFolder.upsert({
create: musicFolder,
update: { name: musicFolder.name },
where: {
uniqueServerFolderId: {
remoteId: musicFolder.remoteId,
serverId: musicFolder.serverId,
},
},
});
});
return ApiSuccess.ok({ data: { ...server } });
return ApiSuccess.ok({ data: {} });
};
const refresh = async (options: { id: number }) => {
@@ -153,6 +216,8 @@ const refresh = async (options: { id: number }) => {
});
}
// mark as deleted if not found
musicFoldersData.forEach(async (musicFolder) => {
await prisma.serverFolder.upsert({
create: musicFolder,
@@ -176,7 +241,7 @@ const fullScan = async (options: {
}) => {
const { id, serverFolderIds } = options;
const server = await prisma.server.findUnique({
include: { serverFolder: true },
include: { serverFolders: true },
where: { id },
});
@@ -187,11 +252,11 @@ const fullScan = async (options: {
let serverFolders;
if (serverFolderIds) {
const selectedServerFolderIds = splitNumberString(serverFolderIds);
serverFolders = server.serverFolder.filter((folder) =>
serverFolders = server.serverFolders.filter((folder) =>
selectedServerFolderIds?.includes(folder.id)
);
} else {
serverFolders = server.serverFolder;
serverFolders = server.serverFolders;
}
if (server.serverType === 'jellyfin') {
@@ -229,8 +294,8 @@ const fullScan = async (options: {
export const serversService = {
create,
findById,
findMany,
fullScan,
getMany,
getOne,
refresh,
};

View File

@@ -0,0 +1,120 @@
import { Request } from 'express';
import { prisma } from '../lib';
import { OffsetPagination, User } from '../types/types';
import {
ApiError,
ApiSuccess,
folderPermissions,
splitNumberString,
} from '../utils';
import { toRes } from './response';
import { SongRequestParams } from './types';
const findById = async (options: { id: number; user: User }) => {
const { id, user } = options;
const album = await prisma.album.findUnique({
include: {
_count: true,
albumArtist: true,
genres: true,
songs: {
include: {
album: true,
artists: true,
externals: true,
genres: true,
images: true,
},
orderBy: [{ disc: 'asc' }, { track: 'asc' }],
},
},
where: { id },
});
if (!album) {
throw ApiError.notFound('');
}
// if (!(await folderPermissions([album?.serverFolderId], user))) {
// throw ApiError.forbidden('');
// }
return ApiSuccess.ok({ data: album });
};
const findMany = async (
req: Request,
options: SongRequestParams & { user: User }
) => {
const {
albumIds: rawAlbumIds,
artistIds: rawArtistIds,
songIds: rawSongIds,
user,
limit,
page,
serverFolderIds: rServerFolderIds,
} = options;
const serverFolderIds = splitNumberString(rServerFolderIds);
const albumIds = splitNumberString(rawAlbumIds);
const artistIds = splitNumberString(rawArtistIds);
const songIds = splitNumberString(rawSongIds);
if (serverFolderIds) {
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 startIndex = limit * page;
const [totalEntries, songs] = await prisma.$transaction([
prisma.song.count({
where: {
OR: [
serverFoldersFilter,
{
albumId: { in: albumIds },
id: { in: songIds },
},
],
},
}),
prisma.song.findMany({
include: {
_count: { select: { favorites: true } },
genres: true,
images: true,
serverFolders: { include: { server: true } },
},
skip: startIndex,
take: limit,
where: { OR: serverFoldersFilter },
}),
]);
return ApiSuccess.ok({
data: songs,
paginationItems: {
limit,
page,
startIndex,
totalEntries,
url: req.originalUrl,
},
});
};
export const songsService = {
findById,
findMany,
};

View File

@@ -0,0 +1,8 @@
import { OffsetPagination } from '../types/types';
export interface SongRequestParams extends OffsetPagination {
albumIds?: string;
artistIds?: string;
serverFolderIds: string;
songIds?: string;
}

View File

@@ -0,0 +1,91 @@
import { prisma } from '../lib';
import { User } from '../types/types';
export enum Roles {
NONE = 0,
GUEST = 1,
USER = 2,
ADMIN = 4,
SUPERADMIN = 8,
}
export enum FolderRoles {
NONE = 0,
READ = 1,
WRITE = 2,
ADMIN = 4,
}
export const folderPermissions = async (serverFolderIds: any[], user: User) => {
if (user.isAdmin) {
return true;
}
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
where: {
OR: [
{
isPublic: true,
},
{
AND: [
{ isPublic: false },
{
serverFolderPermissions: {
some: { userId: { equals: user.id } },
},
},
],
},
],
},
});
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
(serverFolder) => serverFolder.id
);
const hasAccess = serverFolderIds.every((id) =>
serverFoldersWithAccessIds.includes(id)
);
return hasAccess;
};
export const getFolderPermissions = async (user: User) => {
if (user.isAdmin) {
const serverFoldersWithAccess = await prisma.serverFolder.findMany();
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
(serverFolder) => serverFolder.id
);
return serverFoldersWithAccessIds;
}
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
where: {
OR: [
{
isPublic: true,
},
{
AND: [
{ isPublic: false },
{
serverFolderPermissions: {
some: { userId: { equals: user.id } },
},
},
],
},
],
},
});
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
(serverFolder) => serverFolder.id
);
return serverFoldersWithAccessIds;
};

View File

@@ -1,38 +0,0 @@
import { prisma } from '../lib';
import { User } from '../types/types';
export const hasFolderAccess = async (serverFolderIds: any[], user: User) => {
if (user.isAdmin) {
return true;
}
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
where: {
OR: [
{
isPublic: true,
},
{
AND: [
{ isPublic: false },
{
serverFolderPermissions: {
some: { userId: { equals: user.id } },
},
},
],
},
],
},
});
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
(serverFolder) => serverFolder.id
);
const hasAccess = serverFolderIds.every((id) =>
serverFoldersWithAccessIds.includes(id)
);
return hasAccess;
};

View File

@@ -4,7 +4,7 @@ export * from './get-success-response';
export * from './group-by-property';
export * from './split-number-string';
export * from './split-text-string';
export * from './has-folder-access';
export * from './folder-permissions';
export * from './is-array-equal';
export * from './is-json-string';
export * from './validate-request';

View File

@@ -1,8 +1,8 @@
export const randomString = () => {
export const randomString = (length: number) => {
const charSet =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let string = '';
for (let i = 0; i < 12; i += 1) {
for (let i = 0; i < length; i += 1) {
const randomPoz = Math.floor(Math.random() * charSet.length);
string += charSet.substring(randomPoz, randomPoz + 1);
}

View File

@@ -1,4 +1,8 @@
export const splitNumberString = (string: string, delimiter = ',') => {
export const splitNumberString = (string?: string, delimiter = ',') => {
if (!string) {
return undefined;
}
return string.split(delimiter).map((s: string) => {
return Number(s);
});

View File

@@ -3,7 +3,7 @@ import { z } from 'zod';
export const paginationValidation = {
limit: z.preprocess(
(a) => parseInt(z.string().parse(a), 10),
z.number().max(1000)
z.number().min(0).max(1000)
),
page: z.preprocess((a) => parseInt(z.string().parse(a), 10), z.number()),
};