mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
634 lines
14 KiB
TypeScript
634 lines
14 KiB
TypeScript
import { ServerPermissionType, 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, options?: { enabled?: boolean }) => {
|
|
if (user.isAdmin) {
|
|
return prisma.server.findMany({
|
|
include: {
|
|
serverFolders: {
|
|
orderBy: { name: SortOrder.ASC },
|
|
where: { enabled: options?.enabled ? true : undefined },
|
|
},
|
|
serverPermissions: {
|
|
orderBy: { createdAt: SortOrder.ASC },
|
|
where: { userId: user.id },
|
|
},
|
|
serverUrls: true,
|
|
},
|
|
orderBy: { createdAt: SortOrder.ASC },
|
|
});
|
|
}
|
|
|
|
const servers = await prisma.server.findMany({
|
|
include: {
|
|
serverFolders: {
|
|
orderBy: { name: SortOrder.ASC },
|
|
where: {
|
|
OR: [
|
|
// Show all folders if user has server admin permissions
|
|
{
|
|
server: {
|
|
serverPermissions: {
|
|
some: { type: ServerPermissionType.ADMIN, userId: user.id },
|
|
},
|
|
},
|
|
},
|
|
// If not admin, only show folders the user has permissions for
|
|
{ serverFolderPermissions: { some: { userId: user.id } } },
|
|
{
|
|
enabled: options?.enabled ? true : undefined,
|
|
serverFolderPermissions: { some: { userId: user.id } },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
serverPermissions: {
|
|
orderBy: { createdAt: SortOrder.ASC },
|
|
where: { userId: user.id },
|
|
},
|
|
serverUrls: true,
|
|
},
|
|
orderBy: { createdAt: SortOrder.ASC },
|
|
where: { serverPermissions: { some: { userId: user.id } } },
|
|
});
|
|
|
|
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 } },
|
|
},
|
|
});
|
|
|
|
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 navidromeToken = options.token + '||' + options?.altToken;
|
|
|
|
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: navidromeToken,
|
|
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: {
|
|
name?: string;
|
|
noCredential?: boolean;
|
|
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 findScanInProgress = async (options: { serverId: string }) => {
|
|
const tasks = await prisma.task.findMany({
|
|
where: {
|
|
OR: [{ type: TaskType.FULL_SCAN }, { type: TaskType.QUICK_SCAN }],
|
|
completed: false,
|
|
serverId: options.serverId,
|
|
},
|
|
});
|
|
|
|
return tasks;
|
|
};
|
|
|
|
const fullScan = async (
|
|
user: AuthUser,
|
|
options: { id: string; serverFolderId?: string[] }
|
|
) => {
|
|
const { id, serverFolderId } = options;
|
|
|
|
// Only allow scan of enabled folders
|
|
const server = await prisma.server.findUnique({
|
|
include: { serverFolders: { where: { enabled: 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: {
|
|
server: { connect: { id: server.id } },
|
|
type: TaskType.FULL_SCAN,
|
|
user: { connect: { id: user.id } },
|
|
},
|
|
});
|
|
|
|
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 task;
|
|
};
|
|
|
|
const findServerUrlById = async (options: { id: string }) => {
|
|
const serverUrl = await prisma.serverUrl.findUnique({
|
|
where: { id: options.id },
|
|
});
|
|
|
|
return serverUrl;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
const findFolderById = async (options: { id: string }) => {
|
|
const url = await prisma.serverFolder.findUnique({
|
|
where: { id: options.id },
|
|
});
|
|
|
|
if (!url) {
|
|
throw ApiError.notFound('Folder not found.');
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
const deleteFolderById = async (options: { id: string }) => {
|
|
return null;
|
|
};
|
|
|
|
const enableFolderById = async (options: { id: string }) => {
|
|
await prisma.serverFolder.update({
|
|
data: { enabled: true },
|
|
where: { id: options.id },
|
|
});
|
|
|
|
return null;
|
|
};
|
|
|
|
const disableFolderById = async (options: { id: string }) => {
|
|
await prisma.serverFolder.update({
|
|
data: { enabled: false },
|
|
where: { id: options.id },
|
|
});
|
|
|
|
return null;
|
|
};
|
|
|
|
const addPermission = async (options: {
|
|
serverId: string;
|
|
type: ServerPermissionType;
|
|
userId: string;
|
|
}) => {
|
|
const { serverId, userId, type } = options;
|
|
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
});
|
|
|
|
if (!user) {
|
|
throw ApiError.notFound('User not found.');
|
|
}
|
|
|
|
const permission = await prisma.serverPermission.create({
|
|
data: {
|
|
serverId,
|
|
type,
|
|
userId,
|
|
},
|
|
});
|
|
|
|
return permission;
|
|
};
|
|
|
|
const deletePermission = async (options: { id: string }) => {
|
|
await prisma.serverPermission.delete({
|
|
where: { id: options.id },
|
|
});
|
|
|
|
return null;
|
|
};
|
|
|
|
const updateServerPermission = async (options: {
|
|
id: string;
|
|
type: ServerPermissionType;
|
|
}) => {
|
|
const { type, id } = options;
|
|
|
|
const permission = await prisma.serverPermission.update({
|
|
data: { type },
|
|
where: { id },
|
|
});
|
|
|
|
return permission;
|
|
};
|
|
|
|
const addFolderPermission = async (options: {
|
|
serverFolderId: string;
|
|
userId: string;
|
|
}) => {
|
|
const { serverFolderId, userId } = options;
|
|
|
|
const permission = await prisma.serverFolderPermission.create({
|
|
data: {
|
|
serverFolderId,
|
|
userId,
|
|
},
|
|
});
|
|
|
|
return permission;
|
|
};
|
|
|
|
const deleteFolderPermission = async (options: { id: string }) => {
|
|
await prisma.serverFolderPermission.delete({
|
|
where: { id: options.id },
|
|
});
|
|
|
|
return null;
|
|
};
|
|
|
|
export const serversService = {
|
|
addFolderPermission,
|
|
addPermission,
|
|
create,
|
|
createUrl,
|
|
deleteById,
|
|
deleteFolderById,
|
|
deleteFolderPermission,
|
|
deletePermission,
|
|
deleteUrlById,
|
|
disableFolderById,
|
|
disableUrlById,
|
|
enableFolderById,
|
|
enableUrlById,
|
|
findById,
|
|
findFolderById,
|
|
findMany,
|
|
findScanInProgress,
|
|
findServerUrlById,
|
|
findUrlById,
|
|
fullScan,
|
|
refresh,
|
|
remoteServerLogin,
|
|
update,
|
|
updateServerPermission,
|
|
};
|