mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-14 04:20:07 +02:00
Move server directory outside of frontend src
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
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: string; user: User }) => {
|
||||
const { id, user } = options;
|
||||
const albumArtist = await prisma.albumArtist.findUnique({
|
||||
include: {
|
||||
albums: { include: { songs: true } },
|
||||
genres: true,
|
||||
images: true,
|
||||
serverFolders: true,
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!albumArtist) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
const serverFolderIds = albumArtist.serverFolders.map(
|
||||
(serverFolder) => serverFolder.id
|
||||
);
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds, user))) {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
return albumArtist;
|
||||
};
|
||||
|
||||
const findMany = async (
|
||||
req: Request,
|
||||
options: { serverFolderIds: string; user: User } & OffsetPagination
|
||||
) => {
|
||||
const { user, take, serverFolderIds: rServerFolderIds, skip } = options;
|
||||
const serverFolderIds = rServerFolderIds.split(',');
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds!, user))) {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
|
||||
serverFolders: { some: { id: { equals: serverFolderId } } },
|
||||
}));
|
||||
|
||||
const [totalEntries, albumArtists] = await prisma.$transaction([
|
||||
prisma.albumArtist.count({
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
prisma.albumArtist.findMany({
|
||||
include: { genres: true },
|
||||
skip,
|
||||
take,
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { data: albumArtists, totalEntries };
|
||||
};
|
||||
|
||||
export const albumArtistsService = {
|
||||
findById,
|
||||
findMany,
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
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 (user: AuthUser, options: { id: string }) => {
|
||||
const { id } = options;
|
||||
|
||||
const album = await prisma.album.findUnique({
|
||||
include: helpers.albums.include({ songs: true }),
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!album) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
const serverFolderId = album.serverFolders.map((s) => s.id);
|
||||
helpers.shared.checkServerFolderPermissions(user, { serverFolderId });
|
||||
|
||||
return album;
|
||||
};
|
||||
|
||||
export type AlbumFindManyOptions = {
|
||||
orderBy: SortOrder;
|
||||
serverFolderId?: string[];
|
||||
serverId: string;
|
||||
sortBy: AlbumSort;
|
||||
user: AuthUser;
|
||||
} & OffsetPagination;
|
||||
|
||||
const findMany = async (options: AlbumFindManyOptions) => {
|
||||
const { take, serverFolderId, skip, sortBy, orderBy, user, serverId } =
|
||||
options;
|
||||
|
||||
const serverFolderIds =
|
||||
serverFolderId ||
|
||||
(await helpers.shared.getAvailableServerFolderIds(user, { serverId }));
|
||||
|
||||
let totalEntries = 0;
|
||||
let albums;
|
||||
|
||||
if (sortBy === AlbumSort.RATING) {
|
||||
const [count, result] = await prisma.$transaction([
|
||||
prisma.albumRating.count({
|
||||
where: {
|
||||
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||
user: { id: user.id },
|
||||
},
|
||||
}),
|
||||
prisma.albumRating.findMany({
|
||||
include: {
|
||||
album: {
|
||||
include: helpers.albums.include({ songs: false, user }),
|
||||
},
|
||||
},
|
||||
orderBy: { value: orderBy },
|
||||
skip,
|
||||
take,
|
||||
where: {
|
||||
album: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||
user: { id: user.id },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
albums = result.map((rating) => rating.album);
|
||||
totalEntries = count;
|
||||
} else if (sortBy === AlbumSort.FAVORITE) {
|
||||
[totalEntries, albums] = await prisma.$transaction([
|
||||
prisma.album.count({
|
||||
where: {
|
||||
AND: [
|
||||
helpers.shared.serverFolderFilter(serverFolderIds),
|
||||
{ favorites: { some: { userId: user.id } } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
prisma.album.findMany({
|
||||
include: helpers.albums.include({ songs: false, user }),
|
||||
skip,
|
||||
take,
|
||||
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) },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return { data: albums, totalEntries };
|
||||
};
|
||||
|
||||
export const albumsService = {
|
||||
findById,
|
||||
findMany,
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
import { prisma } from '../lib';
|
||||
import { OffsetPagination } from '../types/types';
|
||||
import { ApiError, folderPermissions } from '../utils';
|
||||
|
||||
const findById = async (options: { id: string; user: User }) => {
|
||||
const { id, user } = options;
|
||||
|
||||
const artist = await prisma.artist.findUnique({
|
||||
include: { genres: true, serverFolders: true },
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!artist) throw ApiError.notFound('');
|
||||
|
||||
const serverFolderIds = artist.serverFolders.map(
|
||||
(serverFolder) => serverFolder.id
|
||||
);
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds, user)))
|
||||
throw ApiError.forbidden('');
|
||||
|
||||
return artist;
|
||||
};
|
||||
|
||||
const findMany = async (
|
||||
req: Request,
|
||||
options: { serverFolderIds: string; user: User } & OffsetPagination
|
||||
) => {
|
||||
const { user, skip, take, serverFolderIds: rServerFolderIds } = options;
|
||||
const serverFolderIds = rServerFolderIds.split(',');
|
||||
|
||||
if (!(await folderPermissions(serverFolderIds!, user)))
|
||||
throw ApiError.forbidden('');
|
||||
|
||||
const serverFoldersFilter = serverFolderIds!.map((serverFolderId) => ({
|
||||
serverFolders: { some: { id: { equals: serverFolderId } } },
|
||||
}));
|
||||
|
||||
const [totalEntries, artists] = await prisma.$transaction([
|
||||
prisma.artist.count({
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
prisma.artist.findMany({
|
||||
include: { genres: true },
|
||||
skip,
|
||||
take,
|
||||
where: { OR: serverFoldersFilter },
|
||||
}),
|
||||
]);
|
||||
|
||||
return { data: artists, totalEntries };
|
||||
};
|
||||
|
||||
export const artistsService = {
|
||||
findById,
|
||||
findMany,
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { User } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { prisma } from '../lib';
|
||||
import { generateRefreshToken, generateToken } from '../lib/passport';
|
||||
import { ApiSuccess, randomString } from '../utils';
|
||||
import { ApiError } from '../utils/api-error';
|
||||
|
||||
const login = async (options: { username: string }) => {
|
||||
const { username } = options;
|
||||
const user = await prisma.user.findUnique({
|
||||
include: { serverFolderPermissions: true, serverPermissions: true },
|
||||
where: { username },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw ApiError.notFound('The user does not exist.');
|
||||
}
|
||||
|
||||
const serverPermissions = user.serverPermissions.map((p) => p.id);
|
||||
|
||||
const otherProperties = {
|
||||
isAdmin: user.isAdmin,
|
||||
serverFolderPermissions: user.serverFolderPermissions.map((p) => p.id),
|
||||
serverPermissions,
|
||||
username: user.username,
|
||||
};
|
||||
|
||||
const accessToken = generateToken(user.id, otherProperties);
|
||||
const refreshToken = generateRefreshToken(user.id, otherProperties);
|
||||
|
||||
await prisma.refreshToken.create({
|
||||
data: { token: refreshToken, userId: user.id },
|
||||
});
|
||||
|
||||
return { ...user, accessToken, refreshToken };
|
||||
};
|
||||
|
||||
const register = async (options: { password: string; username: string }) => {
|
||||
const { username, password } = options;
|
||||
const userExists = await prisma.user.findUnique({ where: { username } });
|
||||
|
||||
if (userExists) {
|
||||
throw ApiError.conflict('The user already exists.');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
deviceId: `${username}_${randomString(10)}`,
|
||||
enabled: false,
|
||||
password: hashedPassword,
|
||||
username,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const logout = async (options: { user: User }) => {
|
||||
const { user } = options;
|
||||
await prisma.refreshToken.deleteMany({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
|
||||
return ApiSuccess.noContent({ data: {} });
|
||||
};
|
||||
|
||||
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: string };
|
||||
|
||||
const token = await prisma.refreshToken.findUnique({
|
||||
where: { token: refreshToken },
|
||||
});
|
||||
|
||||
if (!token) throw ApiError.unauthorized('Invalid refresh token.');
|
||||
|
||||
const newToken = generateToken(id);
|
||||
return { accessToken: newToken };
|
||||
};
|
||||
|
||||
export const authService = {
|
||||
login,
|
||||
logout,
|
||||
refresh,
|
||||
register,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,562 @@
|
||||
import { ServerType, TaskType } from '@prisma/client';
|
||||
import { SortOrder } from '@/types/types';
|
||||
import { helpers } from '../helpers';
|
||||
import { prisma } from '../lib';
|
||||
import { AuthUser } from '../middleware';
|
||||
import { subsonic } from '../queue';
|
||||
import { jellyfin } from '../queue/jellyfin';
|
||||
import { navidrome } from '../queue/navidrome';
|
||||
import { ApiError } from '../utils';
|
||||
|
||||
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: [{ serverFolderPermissions: { some: { userId: user.id } } }],
|
||||
},
|
||||
},
|
||||
serverPermissions: {
|
||||
where: { userId: user.id },
|
||||
},
|
||||
},
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!server) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
const findMany = async (user: AuthUser) => {
|
||||
if (user.isAdmin) {
|
||||
return prisma.server.findMany({
|
||||
include: {
|
||||
serverFolders: {
|
||||
orderBy: { name: SortOrder.ASC },
|
||||
},
|
||||
serverPermissions: {
|
||||
orderBy: { createdAt: SortOrder.ASC },
|
||||
where: { userId: user.id },
|
||||
},
|
||||
serverUrls: {
|
||||
include: {
|
||||
userServerUrls: {
|
||||
where: { userId: user.id },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: SortOrder.ASC },
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
token: string;
|
||||
type: ServerType;
|
||||
url: string;
|
||||
username: string;
|
||||
}) => {
|
||||
const isDuplicate = await prisma.server.findUnique({
|
||||
where: { url: options.url },
|
||||
});
|
||||
|
||||
if (isDuplicate) {
|
||||
throw ApiError.conflict('Server already exists.');
|
||||
}
|
||||
|
||||
const serverFolders: {
|
||||
name: string;
|
||||
remoteId: string;
|
||||
serverId: string;
|
||||
}[] = [];
|
||||
|
||||
if (options.type === ServerType.SUBSONIC) {
|
||||
const serverFoldersRes = await subsonic.api.getMusicFolders({
|
||||
token: options.token,
|
||||
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: {
|
||||
...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,
|
||||
serverFolders: { create: serverFoldersCreate },
|
||||
serverUrls: { create: { url: options.url } },
|
||||
token: options.token,
|
||||
type: options.type,
|
||||
url: options.url,
|
||||
username: options.username,
|
||||
},
|
||||
});
|
||||
|
||||
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.JELLYFIN) {
|
||||
const musicFoldersRes = await jellyfin.api.getMusicFolders({
|
||||
remoteUserId: options.remoteUserId,
|
||||
token: options.token,
|
||||
url: options.url,
|
||||
});
|
||||
|
||||
if (!musicFoldersRes) {
|
||||
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,
|
||||
serverFolders: { create: serverFoldersCreate },
|
||||
serverUrls: { create: { url: options.url } },
|
||||
token: options.token,
|
||||
type: options.type,
|
||||
url: options.url,
|
||||
username: options.username,
|
||||
},
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
throw ApiError.badRequest('Server type invalid.');
|
||||
};
|
||||
|
||||
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 serverFolders: {
|
||||
name: string;
|
||||
remoteId: string;
|
||||
serverId: string;
|
||||
}[] = [];
|
||||
|
||||
if (server.type === ServerType.SUBSONIC) {
|
||||
const serverFoldersRes = await subsonic.api.getMusicFolders(server);
|
||||
serverFolders = serverFoldersRes.map((musicFolder) => {
|
||||
return {
|
||||
name: musicFolder.name,
|
||||
remoteId: String(musicFolder.id),
|
||||
serverId: server.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (server.type === ServerType.JELLYFIN) {
|
||||
const musicFoldersRes = await jellyfin.api.getMusicFolders(server);
|
||||
serverFolders = musicFoldersRes.map((musicFolder) => {
|
||||
return {
|
||||
name: musicFolder.Name,
|
||||
remoteId: String(musicFolder.Id),
|
||||
serverId: server.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// mark as deleted if not found
|
||||
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;
|
||||
};
|
||||
|
||||
const fullScan = async (options: { id: string; serverFolderId?: string[] }) => {
|
||||
const { id, serverFolderId } = options;
|
||||
const server = await prisma.server.findUnique({
|
||||
include: { serverFolders: true },
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!server) {
|
||||
throw ApiError.notFound('Server does not exist.');
|
||||
}
|
||||
|
||||
let serverFolders;
|
||||
if (serverFolderId) {
|
||||
serverFolders = server.serverFolders.filter((f) =>
|
||||
serverFolderId?.includes(f.id)
|
||||
);
|
||||
} else {
|
||||
serverFolders = server.serverFolders;
|
||||
}
|
||||
|
||||
if (serverFolders.length === 0) {
|
||||
throw ApiError.notFound('No matching server folders found.');
|
||||
}
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
completed: false,
|
||||
name: 'Full scan',
|
||||
server: { connect: { id: server.id } },
|
||||
type: TaskType.FULL_SCAN,
|
||||
},
|
||||
});
|
||||
|
||||
if (server.type === ServerType.JELLYFIN) {
|
||||
await jellyfin.scanner.scanAll(server, serverFolders, task);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
import { prisma } from '../lib';
|
||||
import { SortOrder } from '../types/types';
|
||||
import { ApiError, ApiSuccess, folderPermissions } from '../utils';
|
||||
// import { toRes } from './response';
|
||||
import { SongRequestParams } from './types';
|
||||
|
||||
const findById = async (options: { id: string; user: User }) => {
|
||||
const { id } = options;
|
||||
|
||||
const album = await prisma.album.findUnique({
|
||||
include: {
|
||||
_count: true,
|
||||
albumArtists: true,
|
||||
genres: true,
|
||||
songs: {
|
||||
include: {
|
||||
album: true,
|
||||
artists: true,
|
||||
externals: true,
|
||||
genres: true,
|
||||
images: true,
|
||||
},
|
||||
orderBy: [
|
||||
{ discNumber: SortOrder.ASC },
|
||||
{ trackNumber: SortOrder.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,
|
||||
serverId,
|
||||
songIds: rawSongIds,
|
||||
user,
|
||||
skip,
|
||||
take,
|
||||
serverFolderIds: rServerFolderIds,
|
||||
} = options;
|
||||
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)))
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
|
||||
// const serverFoldersFilter = serverFolderIds!.map((serverFolderId: number) => {
|
||||
// return { serverFolders: { id: { equals: serverFolderId } } };
|
||||
// });
|
||||
|
||||
// const serverFoldersFilter = {
|
||||
// serverFolders: { some: { id: { in: serverFolderIds } } },
|
||||
// };
|
||||
|
||||
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,
|
||||
take,
|
||||
where: {
|
||||
AND: {
|
||||
// OR: serverFoldersFilter,
|
||||
serverId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return { data: songs, totalEntries };
|
||||
};
|
||||
|
||||
export const songsService = {
|
||||
findById,
|
||||
findMany,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { OffsetPagination } from '../types/types';
|
||||
|
||||
export interface SongRequestParams extends OffsetPagination {
|
||||
albumIds?: string;
|
||||
artistIds?: string;
|
||||
serverFolderIds: string;
|
||||
serverId: string;
|
||||
songIds?: string;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { prisma } from '../lib';
|
||||
import { AuthUser } from '../middleware';
|
||||
import { ApiError } from '../utils';
|
||||
|
||||
const findById = async (user: AuthUser, options: { id: string }) => {
|
||||
const { id } = options;
|
||||
|
||||
if (!user.isAdmin && user.id !== id) {
|
||||
throw ApiError.forbidden();
|
||||
}
|
||||
|
||||
const uniqueUser = await prisma.user.findUnique({
|
||||
include: { serverFolderPermissions: true },
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!uniqueUser) {
|
||||
throw ApiError.notFound('');
|
||||
}
|
||||
|
||||
return uniqueUser;
|
||||
};
|
||||
|
||||
const findMany = async () => {
|
||||
const users = await prisma.user.findMany({});
|
||||
return users;
|
||||
};
|
||||
|
||||
export const usersService = {
|
||||
findById,
|
||||
findMany,
|
||||
};
|
||||
Reference in New Issue
Block a user