mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 04:50:12 +02:00
Move server directory outside of frontend src
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'@typescript-eslint/lines-between-class-members': 'off',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-unresolved': 'error',
|
||||
},
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -1,22 +0,0 @@
|
||||
@serverId =
|
||||
@albumArtistId =
|
||||
|
||||
###
|
||||
# skip: The number of rows to skip before returning rows. Must be a non-negative integer.
|
||||
# take: The number of rows to return. Must be a non-negative integer.
|
||||
# orderBy: asc | desc
|
||||
# sortBy: date_added | date_added_remote | date_released | random | rating | title | year
|
||||
GET {{host}}/api/servers/{{serverId}}/albumArtists
|
||||
?skip=0
|
||||
&take=100
|
||||
&sortBy=title
|
||||
&orderBy=desc
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers/{{serverId}}/albumArtists/{{albumArtistId}}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
@@ -1,22 +0,0 @@
|
||||
@serverId =
|
||||
@albumId =
|
||||
|
||||
###
|
||||
# skip: The number of rows to skip before returning rows. Must be a non-negative integer.
|
||||
# take: The number of rows to return. Must be a non-negative integer.
|
||||
# orderBy: asc | desc
|
||||
# sortBy: date_added | date_added_remote | date_released | random | rating | title | year
|
||||
GET {{host}}/api/servers/{{serverId}}/albums
|
||||
?skip=0
|
||||
&take=100
|
||||
&sortBy=title
|
||||
&orderBy=desc
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers/{{serverId}}/albums/{{albumId}}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
@@ -1,22 +0,0 @@
|
||||
@serverId =
|
||||
@artistId =
|
||||
|
||||
###
|
||||
# skip: The number of rows to skip before returning rows. Must be a non-negative integer.
|
||||
# take: The number of rows to return. Must be a non-negative integer.
|
||||
# orderBy: asc | desc
|
||||
# sortBy: date_added | date_added_remote | date_released | random | rating | title | year
|
||||
GET {{host}}/api/servers/{{serverId}}/artists
|
||||
?skip=0
|
||||
&take=100
|
||||
&sortBy=title
|
||||
&orderBy=desc
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers/{{serverId}}/artists/{{artistId}}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
@@ -1,52 +0,0 @@
|
||||
|
||||
###
|
||||
|
||||
POST {{host}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "{{authUsername}}",
|
||||
"password": "{{authPassword}}"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
POST {{host}}/api/auth/logout
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
{
|
||||
"username": "{{authUsername}}",
|
||||
"password": "{{authPassword}}"
|
||||
}
|
||||
|
||||
|
||||
###
|
||||
|
||||
POST {{host}}/api/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": "{{refreshToken}}"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
# @prompt username Login username
|
||||
# @prompt password Login password
|
||||
POST {{host}}/api/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "{{username}}",
|
||||
"password": "{{password}}"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/auth/ping
|
||||
Content-Type: application/json
|
||||
|
||||
|
||||
@contentType = application/json
|
||||
@serverId =
|
||||
@@ -1,66 +0,0 @@
|
||||
@serverId =
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers/{{serverId}}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers/{{serverId}}/folder
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers/{{serverId}}/refresh
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/servers/{{serverId}}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
# name: Nickname for the server
|
||||
# type: SUBSONIC | JELLYFIN | NAVIDROME
|
||||
# url: The URL of the server e.g. http://192.168.1.1:8096
|
||||
# @prompt username The user which will be used to login and scan from the server
|
||||
# @prompt password The password for the user
|
||||
POST {{host}}/api/servers/
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"name": "My Jellyfin Server",
|
||||
"type": "JELLYFIN",
|
||||
"url": "http://192.168.14.11:8097",
|
||||
"username": "{{username}}",
|
||||
"password": "{{password}}"
|
||||
}
|
||||
|
||||
|
||||
###
|
||||
|
||||
POST {{host}}/api/servers/{{serverId}}/scan
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
{
|
||||
"serverFolderIds": [""]
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
@serverId =
|
||||
|
||||
###
|
||||
# skip: The number of rows to skip before returning rows. Must be a non-negative integer.
|
||||
# take: The number of rows to return. Must be a non-negative integer.
|
||||
# orderBy: asc | desc
|
||||
# sortBy: date_added | date_added_remote | date_released | random | rating | title | year
|
||||
GET {{host}}/api/servers/{{serverId}}/songs
|
||||
?skip=0
|
||||
&take=100
|
||||
&sortBy=title
|
||||
&orderBy=desc
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
@@ -1,21 +0,0 @@
|
||||
@userId =
|
||||
|
||||
###
|
||||
# skip: The number of rows to skip before returning rows. Must be a non-negative integer.
|
||||
# take: The number of rows to return. Must be a non-negative integer.
|
||||
# orderBy: asc | desc
|
||||
# sortBy: date_added | date_added_remote | date_released | random | rating | title | year
|
||||
GET {{host}}/api/users
|
||||
?skip=0
|
||||
&take=100
|
||||
&sortBy=title
|
||||
&orderBy=desc
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET {{host}}/api/users/{{userId}}
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {{token}}
|
||||
@@ -1,28 +0,0 @@
|
||||
FROM node:16.5-alpine
|
||||
|
||||
ARG DATABASE_PORT
|
||||
|
||||
ADD docker-entrypoint.sh /
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
COPY ./wait-for-it.sh /wait-for-it.sh
|
||||
RUN chmod +x /wait-for-it.sh
|
||||
|
||||
# Change directory so that our commands run inside this new directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency definitions
|
||||
COPY package.*json ./
|
||||
COPY prisma ./
|
||||
|
||||
# Install dependecies
|
||||
RUN npm install
|
||||
|
||||
# Get all the code needed to run the app
|
||||
COPY . .
|
||||
|
||||
# Expose the port the app runs in
|
||||
EXPOSE 9321
|
||||
|
||||
# Serve the app
|
||||
ENTRYPOINT ./docker-entrypoint.sh $DATABASE_PORT
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
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 albumArtists = await service.albumArtists.findMany(req, {
|
||||
serverFolderIds: String(serverFolderIds),
|
||||
skip: Number(skip),
|
||||
take: Number(take),
|
||||
user: req.authUser,
|
||||
});
|
||||
|
||||
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 getDetail = async (
|
||||
req: TypedRequest<typeof validation.albumArtists.detail>,
|
||||
res: Response
|
||||
) => {
|
||||
const { id } = req.params;
|
||||
const albumArtist = await service.albumArtists.findById({
|
||||
id,
|
||||
user: req.authUser,
|
||||
});
|
||||
|
||||
const success = ApiSuccess.ok({ data: albumArtist });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
export const albumArtistsController = {
|
||||
getDetail,
|
||||
getList,
|
||||
};
|
||||
@@ -1,102 +0,0 @@
|
||||
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 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],
|
||||
});
|
||||
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const getList = 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));
|
||||
};
|
||||
|
||||
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 = {
|
||||
getDetail,
|
||||
getDetailSongList,
|
||||
getList,
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
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 artist = await service.artists.findById({
|
||||
id,
|
||||
user: req.authUser,
|
||||
});
|
||||
|
||||
const success = ApiSuccess.ok({ data: artist });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const getList = async (
|
||||
req: TypedRequest<typeof validation.artists.list>,
|
||||
res: Response
|
||||
) => {
|
||||
const { take, skip, serverFolderId } = req.query;
|
||||
|
||||
// const artists = await service.artists.findMany(req, {
|
||||
// serverFolderIds: String(serverFolderIds),
|
||||
// skip: Number(skip),
|
||||
// take: Number(take),
|
||||
// user: req.authUser,
|
||||
// });
|
||||
|
||||
// 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 = {
|
||||
getDetail,
|
||||
getList,
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
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';
|
||||
|
||||
const login = async (
|
||||
req: TypedRequest<typeof validation.auth.login>,
|
||||
res: Response
|
||||
) => {
|
||||
const { username } = req.body;
|
||||
const user = await service.auth.login({ username });
|
||||
|
||||
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const register = async (
|
||||
req: TypedRequest<typeof validation.auth.register>,
|
||||
res: Response
|
||||
) => {
|
||||
const { username, password } = req.body;
|
||||
const user = await service.auth.register({
|
||||
password,
|
||||
username,
|
||||
});
|
||||
|
||||
const success = ApiSuccess.ok({ data: toApiModel.users([user])[0] });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const logout = async (req: Request, res: Response) => {
|
||||
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) => {
|
||||
return res.status(200).json(
|
||||
getSuccessResponse({
|
||||
data: {
|
||||
description: packageJson.description,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
},
|
||||
statusCode: 200,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const refresh = async (
|
||||
req: TypedRequest<typeof validation.auth.refresh>,
|
||||
res: Response
|
||||
) => {
|
||||
const refresh = await service.auth.refresh({
|
||||
refreshToken: req.body.refreshToken,
|
||||
});
|
||||
|
||||
const success = ApiSuccess.ok({ data: refresh });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
export const authController = { login, logout, ping, refresh, register };
|
||||
@@ -1,17 +0,0 @@
|
||||
import { albumArtistsController } from './album-artists.controller';
|
||||
import { albumsController } from './albums.controller';
|
||||
import { artistsController } from './artists.controller';
|
||||
import { authController } from './auth.controller';
|
||||
import { serversController } from './servers.controller';
|
||||
import { songsController } from './songs.controller';
|
||||
import { usersController } from './users.controller';
|
||||
|
||||
export const controller = {
|
||||
albumArtists: albumArtistsController,
|
||||
albums: albumsController,
|
||||
artists: artistsController,
|
||||
auth: authController,
|
||||
servers: serversController,
|
||||
songs: songsController,
|
||||
users: usersController,
|
||||
};
|
||||
@@ -1,177 +0,0 @@
|
||||
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 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 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,
|
||||
});
|
||||
|
||||
const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const updateServer = async (
|
||||
req: TypedRequest<typeof validation.servers.update>,
|
||||
res: Response
|
||||
) => {
|
||||
const { serverId } = req.params;
|
||||
const { username, password, name, legacy, type, url } = req.body;
|
||||
|
||||
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 refreshServer = async (
|
||||
req: TypedRequest<typeof validation.servers.refresh>,
|
||||
res: Response
|
||||
) => {
|
||||
const { serverId } = req.params;
|
||||
const data = await service.servers.refresh({ id: serverId });
|
||||
|
||||
const success = ApiSuccess.ok({ data: toApiModel.servers([data])[0] });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
const scanServer = async (
|
||||
req: TypedRequest<typeof validation.servers.scan>,
|
||||
res: Response
|
||||
) => {
|
||||
const { serverId } = req.params;
|
||||
const { serverFolderId } = req.body;
|
||||
|
||||
const data = await service.servers.fullScan({
|
||||
id: serverId,
|
||||
serverFolderId,
|
||||
});
|
||||
|
||||
const success = ApiSuccess.ok({ data });
|
||||
return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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,
|
||||
createServerUrl,
|
||||
deleteServer,
|
||||
deleteServerUrl,
|
||||
disableServerUrl,
|
||||
enableServerUrl,
|
||||
getServerDetail,
|
||||
getServerList,
|
||||
refreshServer,
|
||||
scanServer,
|
||||
updateServer,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
const getSongList = async (req: Request, res: Response) => {
|
||||
const { serverId } = req.params;
|
||||
const { take, skip, serverFolderId } = req.query;
|
||||
|
||||
// const songs = await songsService.findMany(req, {
|
||||
// serverFolderIds: String(serverFolderId),
|
||||
// serverId,
|
||||
// skip: Number(skip),
|
||||
// take: Number(take),
|
||||
// user: req.authUser,
|
||||
// });
|
||||
|
||||
// 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 {};
|
||||
|
||||
// return res.status(data.statusCode).json(getSuccessResponse(data));
|
||||
};
|
||||
|
||||
export const songsController = {
|
||||
getSongList,
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ApiSuccess, getSuccessResponse } from '@/utils';
|
||||
import { toApiModel } from '@helpers/api-model';
|
||||
import { service } from '@services/index';
|
||||
|
||||
const getUserDetail = async (req: Request, res: Response) => {
|
||||
const { id } = req.params;
|
||||
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 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 = {
|
||||
getUserDetail,
|
||||
getUserList,
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
apk add --no-cache bash
|
||||
|
||||
./wait-for-it.sh db:$1 --timeout=20 --strict -- echo "db is up"
|
||||
|
||||
npx prisma generate
|
||||
npx prisma migrate deploy
|
||||
npx ts-node prisma/seed.ts
|
||||
|
||||
npm run dev
|
||||
@@ -1,86 +0,0 @@
|
||||
import { AuthUser } from '@/middleware';
|
||||
import { SortOrder } from '@/types/types';
|
||||
import { songHelpers } from '@helpers/songs.helpers';
|
||||
|
||||
export enum AlbumSort {
|
||||
DATE_ADDED = 'added',
|
||||
DATE_ADDED_REMOTE = 'addedRemote',
|
||||
DATE_RELEASED = 'released',
|
||||
DATE_RELEASED_YEAR = 'year',
|
||||
FAVORITE = 'favorite',
|
||||
NAME = 'name',
|
||||
RANDOM = 'random',
|
||||
RATING = 'rating',
|
||||
}
|
||||
|
||||
const include = (options: { songs?: boolean; user?: AuthUser }) => {
|
||||
// Prisma.AlbumInclude
|
||||
const props = {
|
||||
_count: {
|
||||
select: {
|
||||
favorites: true,
|
||||
songs: true,
|
||||
},
|
||||
},
|
||||
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;
|
||||
};
|
||||
|
||||
const sort = (sortBy: AlbumSort, orderBy: SortOrder) => {
|
||||
let order;
|
||||
|
||||
switch (sortBy) {
|
||||
case AlbumSort.NAME:
|
||||
order = { name: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.DATE_ADDED:
|
||||
order = { createdAt: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.DATE_ADDED_REMOTE:
|
||||
order = { remoteCreatedAt: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.DATE_RELEASED:
|
||||
order = { releaseDate: orderBy, year: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.DATE_RELEASED_YEAR:
|
||||
order = { releaseYear: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.RATING:
|
||||
order = { rating: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.FAVORITE:
|
||||
order = { favorite: orderBy };
|
||||
break;
|
||||
|
||||
default:
|
||||
order = { title: orderBy };
|
||||
break;
|
||||
}
|
||||
|
||||
return order;
|
||||
};
|
||||
|
||||
export const albumHelpers = {
|
||||
include,
|
||||
sort,
|
||||
};
|
||||
@@ -1,520 +0,0 @@
|
||||
/* 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,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { albumHelpers } from './albums.helpers';
|
||||
import { sharedHelpers } from './shared.helpers';
|
||||
import { songHelpers } from './songs.helpers';
|
||||
|
||||
export const helpers = {
|
||||
albums: albumHelpers,
|
||||
shared: sharedHelpers,
|
||||
songs: songHelpers,
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
import { AuthUser } from '@/middleware';
|
||||
import { ApiError } from '@/utils';
|
||||
import { prisma } from '@lib/prisma';
|
||||
|
||||
const checkServerPermissions = (
|
||||
user: AuthUser,
|
||||
options: { serverId?: string }
|
||||
) => {
|
||||
const { serverId } = options;
|
||||
|
||||
if (user.isAdmin || !serverId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverId && !user.flatServerPermissions.includes(serverId)) {
|
||||
throw ApiError.forbidden();
|
||||
}
|
||||
};
|
||||
|
||||
const checkServerFolderPermissions = (
|
||||
user: AuthUser,
|
||||
options: { serverFolderId?: string[] | string }
|
||||
) => {
|
||||
const { serverFolderId } = options;
|
||||
|
||||
if (user.isAdmin || !serverFolderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ids: string[] = [];
|
||||
if (typeof serverFolderId === 'string') {
|
||||
ids = [serverFolderId];
|
||||
} else if (typeof serverFolderId === 'object') {
|
||||
ids = serverFolderId;
|
||||
}
|
||||
|
||||
for (const id of ids) {
|
||||
if (!user.flatServerFolderPermissions.includes(id)) {
|
||||
throw ApiError.forbidden('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableServerFolderIds = async (
|
||||
user: AuthUser,
|
||||
options: { serverId: string }
|
||||
) => {
|
||||
const { serverId } = options;
|
||||
|
||||
if (user.isAdmin) {
|
||||
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||
where: { serverId },
|
||||
});
|
||||
|
||||
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
|
||||
(serverFolder) => serverFolder.id
|
||||
);
|
||||
|
||||
return serverFoldersWithAccessIds;
|
||||
}
|
||||
|
||||
const serverFoldersWithAccess = await prisma.serverFolder.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
serverFolderPermissions: {
|
||||
some: { userId: { equals: user.id } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
|
||||
(serverFolder) => serverFolder.id
|
||||
);
|
||||
|
||||
return serverFoldersWithAccessIds;
|
||||
};
|
||||
|
||||
const serverFolderFilter = (serverFolderIds: string[]) => {
|
||||
return {
|
||||
serverFolders: { every: { id: { in: serverFolderIds } } },
|
||||
};
|
||||
};
|
||||
|
||||
const paginationParams = (options: { skip: any; take: any }) => {
|
||||
const { skip, take } = options;
|
||||
|
||||
return {
|
||||
skip: Number(skip),
|
||||
take: Number(take),
|
||||
};
|
||||
};
|
||||
|
||||
export const sharedHelpers = {
|
||||
checkServerFolderPermissions,
|
||||
checkServerPermissions,
|
||||
getAvailableServerFolderIds,
|
||||
params: {
|
||||
pagination: paginationParams,
|
||||
},
|
||||
serverFolderFilter,
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
const include = () => {
|
||||
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: [
|
||||
// { albumId: Prisma.SortOrder.asc },
|
||||
{ discNumber: Prisma.SortOrder.asc },
|
||||
{ trackNumber: Prisma.SortOrder.asc },
|
||||
],
|
||||
};
|
||||
|
||||
return props;
|
||||
};
|
||||
|
||||
export const songHelpers = {
|
||||
findMany,
|
||||
include,
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './prisma';
|
||||
export { default as throttle } from './throttle';
|
||||
@@ -1,100 +0,0 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import passport from 'passport';
|
||||
import {
|
||||
Strategy as JwtStrategy,
|
||||
ExtractJwt,
|
||||
StrategyOptions,
|
||||
} from 'passport-jwt';
|
||||
import { Strategy as LocalStrategy } from 'passport-local';
|
||||
import { prisma } from './prisma';
|
||||
|
||||
export const generateToken = (
|
||||
id: string,
|
||||
otherProperties?: { [key: string]: any }
|
||||
) => {
|
||||
return jwt.sign(
|
||||
{ id, ...otherProperties },
|
||||
String(process.env.TOKEN_SECRET),
|
||||
{
|
||||
expiresIn: String(process.env.TOKEN_EXPIRATION || '15m'),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const generateRefreshToken = (
|
||||
id: string,
|
||||
otherProperties?: { [key: string]: any }
|
||||
) => {
|
||||
return jwt.sign(
|
||||
{ id, ...otherProperties },
|
||||
String(process.env.TOKEN_SECRET),
|
||||
{
|
||||
expiresIn: String(process.env.TOKEN_REFRESH_EXPIRATION || '90d'),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const authenticateUser = async (
|
||||
username: string,
|
||||
password: string,
|
||||
done: any
|
||||
) => {
|
||||
const user = await prisma.user.findUnique({ where: { username } });
|
||||
|
||||
if (user === null || user === undefined) {
|
||||
return done(null, false);
|
||||
}
|
||||
|
||||
if (!user.enabled) {
|
||||
return done(null, false, { message: 'The user is not enabled.' });
|
||||
}
|
||||
|
||||
if (await bcrypt.compare(password, user.password)) {
|
||||
return done(null, user);
|
||||
}
|
||||
|
||||
return done(null, false, { message: 'Invalid credentials.' });
|
||||
};
|
||||
|
||||
passport.use(new LocalStrategy(authenticateUser));
|
||||
|
||||
const jwtOptions: StrategyOptions = {
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: String(process.env.TOKEN_SECRET),
|
||||
};
|
||||
|
||||
passport.use(
|
||||
new JwtStrategy(jwtOptions, async (jwt_payload: any, done: any) => {
|
||||
await prisma.user
|
||||
.findUnique({
|
||||
include: {
|
||||
serverFolderPermissions: true,
|
||||
serverPermissions: true,
|
||||
},
|
||||
where: { id: jwt_payload.id },
|
||||
})
|
||||
.then((user) => {
|
||||
// eslint-disable-next-line promise/no-callback-in-promise
|
||||
return done(null, user);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err.message);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
passport.serializeUser((user: any, done) => {
|
||||
return done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser(async (id: string, done) => {
|
||||
return done(
|
||||
null,
|
||||
await prisma.user.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient({ errorFormat: 'minimal' });
|
||||
export const exclude = <T, Key extends keyof T>(
|
||||
resultSet: T,
|
||||
...keys: Key[]
|
||||
): Omit<T, Key> => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const key of keys) {
|
||||
delete resultSet[key];
|
||||
}
|
||||
return resultSet;
|
||||
};
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
prisma.$use(async (params, next) => {
|
||||
const maxRetries = 3;
|
||||
let retries = 0;
|
||||
|
||||
do {
|
||||
try {
|
||||
const result = await next(params);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.log('err', err);
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (err.code === 'P2002') {
|
||||
retries = 3; // Don't retry on unique constraint violation
|
||||
return null;
|
||||
}
|
||||
}
|
||||
retries += 1;
|
||||
return sleep(100);
|
||||
}
|
||||
} while (retries < maxRetries);
|
||||
});
|
||||
|
||||
// prisma.$use(async (params, next) => {
|
||||
// const before = Date.now();
|
||||
|
||||
// const result = await next(params);
|
||||
|
||||
// const after = Date.now();
|
||||
|
||||
// console.log(
|
||||
// `Query ${params.model}.${params.action} took ${after - before}ms`
|
||||
// );
|
||||
|
||||
// return result;
|
||||
// });
|
||||
@@ -1,8 +0,0 @@
|
||||
import pThrottle from 'p-throttle';
|
||||
|
||||
const throttle = pThrottle({
|
||||
interval: 1000,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
export default throttle;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
export const authenticateAdmin = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (!req.authUser.isAdmin) {
|
||||
return res.status(403).json({
|
||||
error: {
|
||||
message: 'This action requires an administrator account.',
|
||||
path: req.path,
|
||||
},
|
||||
response: 'Error',
|
||||
statusCode: 403,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import { ServerFolderPermission, ServerPermission, User } from '@prisma/client';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import passport from 'passport';
|
||||
|
||||
export type AuthUser = User & {
|
||||
flatServerFolderPermissions: string[];
|
||||
flatServerPermissions: string[];
|
||||
serverFolderPermissions: ServerFolderPermission[];
|
||||
serverId?: string;
|
||||
serverPermissions: ServerPermission[];
|
||||
};
|
||||
|
||||
export const authenticate = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
passport.authenticate('jwt', { session: false }, (err, user, info) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
message: info?.message || 'Invalid authorization.',
|
||||
path: req.path,
|
||||
},
|
||||
response: 'Error',
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.enabled) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
message: 'Your account is not enabled.',
|
||||
path: req.path,
|
||||
},
|
||||
response: 'Error',
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
|
||||
const flatServerFolderPermissions = user.serverFolderPermissions.map(
|
||||
(permission: ServerFolderPermission) => permission.serverFolderId
|
||||
);
|
||||
|
||||
const flatServerPermissions = user.serverPermissions.map(
|
||||
(permission: ServerPermission) => permission.serverId
|
||||
);
|
||||
|
||||
const props = {
|
||||
createdAt: user?.createdAt,
|
||||
enabled: user?.enabled,
|
||||
flatServerFolderPermissions,
|
||||
flatServerPermissions,
|
||||
id: user?.id,
|
||||
isAdmin: user?.isAdmin,
|
||||
server: req.params.serverId,
|
||||
serverFolderPermissions: user?.serverFolderPermissions,
|
||||
serverPermissions: user?.serverPermissions,
|
||||
updatedAt: user?.updatedAt,
|
||||
username: user?.username,
|
||||
};
|
||||
|
||||
req.authUser = props;
|
||||
|
||||
return next();
|
||||
})(req, res, next);
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { isJsonString } from '@utils/is-json-string';
|
||||
|
||||
export const errorHandler = (
|
||||
err: any,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
let message = '';
|
||||
|
||||
const trace = err.stack?.match(/at .* \(.*\)/g).map((e: string) => {
|
||||
return e.replace(/\(|\)/g, '');
|
||||
});
|
||||
|
||||
if (err.message) {
|
||||
message = isJsonString(err.message)
|
||||
? Array.isArray(JSON.parse(err.message))
|
||||
? JSON.parse(err.message)[0].message // Handles errors sent from zod preprocess
|
||||
: JSON.parse(err.message)
|
||||
: err.message;
|
||||
}
|
||||
|
||||
res.status(err.statusCode || 500).json({
|
||||
error: {
|
||||
message,
|
||||
path: req.path,
|
||||
trace,
|
||||
},
|
||||
response: 'Error',
|
||||
statusCode: err.statusCode || 500,
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './error-handler';
|
||||
export * from './authenticate';
|
||||
export * from './authenticate-admin';
|
||||
Generated
-6874
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "sonixd-server",
|
||||
"version": "1.0.0-alpha1",
|
||||
"description": "A full-featured Subsonic/Jellyfin compatible music player",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon --legacy-watch -e ts,js --exec ts-node -r tsconfig-paths/register server.ts",
|
||||
"prod": "ts-node -r tsconfig-paths/register server.ts",
|
||||
"dev:debug": "nodemon --config nodemon.json --inspect-brk server.ts",
|
||||
"build": "tsc --project . && tsconfig-replace-paths --project tsconfig.json"
|
||||
},
|
||||
"keywords": [
|
||||
"subsonic",
|
||||
"navidrome",
|
||||
"airsonic",
|
||||
"jellyfin",
|
||||
"react",
|
||||
"electron"
|
||||
],
|
||||
"author": {
|
||||
"name": "jeffvli",
|
||||
"url": "https://github.com/jeffvli/"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed.ts"
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/node": "^18.8.4",
|
||||
"@types/passport-jwt": "^3.0.7",
|
||||
"@types/passport-local": "^1.0.34",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"prisma": "^4.5.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"tsconfig-replace-paths": "^0.0.11",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^4.5.0",
|
||||
"@types/better-queue": "^3.8.3",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-queue": "^3.8.12",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"md5": "^2.3.0",
|
||||
"p-throttle": "^4.1.1",
|
||||
"passport": "^0.4.1",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"zod": "^3.19.1"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
FROM node:16.5-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./ ./prisma/
|
||||
|
||||
CMD ["npx", "prisma", "studio"]
|
||||
@@ -1,938 +0,0 @@
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ServerType" AS ENUM ('SUBSONIC', 'JELLYFIN', 'NAVIDROME');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ServerPermissionType" AS ENUM ('ADMIN', 'EDITOR', 'VIEWER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExternalSource" AS ENUM ('MUSICBRAINZ', 'LASTFM', 'THEAUDIODB', 'SPOTIFY');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ExternalType" AS ENUM ('ID', 'LINK');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ImageType" AS ENUM ('PRIMARY', 'BACKDROP', 'LOGO', 'SCREENSHOT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TaskType" AS ENUM ('FULL_SCAN', 'QUICK_SCAN', 'REFRESH', 'SPOTIFY', 'MUSICBRAINZ', 'LASTFM');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RefreshToken" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"token" TEXT NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"deviceId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "History" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"userId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "History_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Server" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"remoteUserId" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"type" "ServerType" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Server_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Folder" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"parentId" UUID,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "Folder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ServerPermission" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"type" "ServerPermissionType" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "ServerPermission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ServerUrl" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"url" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "ServerUrl_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserServerUrl" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"serverUrlId" UUID NOT NULL,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "UserServerUrl_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ServerFolder" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"remoteId" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"lastScannedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "ServerFolder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ServerFolderPermission" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"serverFolderId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "ServerFolderPermission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Genre" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Genre_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AlbumArtistFavorite" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"albumArtistId" UUID NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "AlbumArtistFavorite_pkey" PRIMARY KEY ("userId","albumArtistId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ArtistFavorite" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"artistId" UUID NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "ArtistFavorite_pkey" PRIMARY KEY ("userId","artistId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AlbumFavorite" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"albumId" UUID NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "AlbumFavorite_pkey" PRIMARY KEY ("userId","albumId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SongFavorite" (
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"songId" UUID NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "SongFavorite_pkey" PRIMARY KEY ("userId","songId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AlbumArtistRating" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"albumArtistId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "AlbumArtistRating_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ArtistRating" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"artistId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "ArtistRating_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AlbumRating" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"albumId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "AlbumRating_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SongRating" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"value" DOUBLE PRECISION NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"userId" UUID NOT NULL,
|
||||
"songId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "SongRating_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Image" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"url" TEXT,
|
||||
"remoteUrl" TEXT NOT NULL,
|
||||
"type" "ImageType" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Image_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "External" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"value" TEXT NOT NULL,
|
||||
"type" "ExternalType" NOT NULL,
|
||||
"source" "ExternalSource" NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "External_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AlbumArtist" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"sortName" TEXT NOT NULL,
|
||||
"biography" TEXT,
|
||||
"remoteId" TEXT NOT NULL,
|
||||
"remoteCreatedAt" TIMESTAMP(3),
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "AlbumArtist_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Album" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"sortName" TEXT NOT NULL,
|
||||
"releaseDate" TIMESTAMP(3),
|
||||
"releaseYear" INTEGER,
|
||||
"remoteId" TEXT NOT NULL,
|
||||
"remoteCreatedAt" TIMESTAMP(3),
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "Album_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Artist" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"sortName" TEXT NOT NULL,
|
||||
"biography" TEXT,
|
||||
"remoteId" TEXT NOT NULL,
|
||||
"remoteCreatedAt" TIMESTAMP(3),
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "Artist_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Song" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"sortName" TEXT NOT NULL,
|
||||
"releaseDate" TIMESTAMP(3),
|
||||
"releaseYear" INTEGER,
|
||||
"duration" DOUBLE PRECISION NOT NULL,
|
||||
"size" INTEGER,
|
||||
"lyrics" TEXT,
|
||||
"bitRate" INTEGER NOT NULL,
|
||||
"container" TEXT NOT NULL,
|
||||
"discNumber" INTEGER NOT NULL DEFAULT 1,
|
||||
"trackNumber" INTEGER,
|
||||
"artistName" TEXT,
|
||||
"remoteId" TEXT NOT NULL,
|
||||
"remoteCreatedAt" TIMESTAMP(3),
|
||||
"deleted" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"albumArtistId" UUID,
|
||||
"albumId" UUID,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "Song_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Task" (
|
||||
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" TEXT NOT NULL,
|
||||
"type" "TaskType" NOT NULL,
|
||||
"message" TEXT,
|
||||
"progress" TEXT,
|
||||
"completed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isError" BOOLEAN DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"serverId" UUID NOT NULL,
|
||||
|
||||
CONSTRAINT "Task_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_HistoryToSong" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_FolderToSong" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_FolderToServerFolder" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ServerFolderToSong" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_GenreToSong" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ImageToSong" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ExternalToSong" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumArtistToGenre" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumArtistToExternal" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumArtistToServerFolder" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumArtistToImage" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumToGenre" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumToArtist" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumToAlbumArtist" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumToExternal" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumToServerFolder" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_AlbumToImage" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ArtistToGenre" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ArtistToSong" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ArtistToExternal" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ArtistToServerFolder" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_ArtistToImage" (
|
||||
"A" UUID NOT NULL,
|
||||
"B" UUID NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_deviceId_key" ON "User"("deviceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Server_url_key" ON "Server"("url");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Folder_path_key" ON "Folder"("path");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Folder_serverId_path_key" ON "Folder"("serverId", "path");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ServerPermission_userId_serverId_key" ON "ServerPermission"("userId", "serverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ServerUrl_serverId_url_key" ON "ServerUrl"("serverId", "url");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserServerUrl_userId_serverId_key" ON "UserServerUrl"("userId", "serverId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ServerFolder_remoteId_key" ON "ServerFolder"("remoteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ServerFolder_serverId_remoteId_key" ON "ServerFolder"("serverId", "remoteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ServerFolderPermission_userId_serverFolderId_key" ON "ServerFolderPermission"("userId", "serverFolderId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Genre_name_key" ON "Genre"("name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AlbumArtistFavorite_userId_albumArtistId_key" ON "AlbumArtistFavorite"("userId", "albumArtistId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ArtistFavorite_userId_artistId_key" ON "ArtistFavorite"("userId", "artistId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AlbumFavorite_userId_albumId_key" ON "AlbumFavorite"("userId", "albumId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SongFavorite_userId_songId_key" ON "SongFavorite"("userId", "songId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AlbumArtistRating_userId_albumArtistId_key" ON "AlbumArtistRating"("userId", "albumArtistId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ArtistRating_userId_artistId_key" ON "ArtistRating"("userId", "artistId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AlbumRating_userId_albumId_key" ON "AlbumRating"("userId", "albumId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "SongRating_userId_songId_key" ON "SongRating"("userId", "songId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Image_remoteUrl_type_key" ON "Image"("remoteUrl", "type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "External_value_source_key" ON "External"("value", "source");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AlbumArtist_serverId_remoteId_key" ON "AlbumArtist"("serverId", "remoteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Album_serverId_remoteId_key" ON "Album"("serverId", "remoteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Artist_serverId_remoteId_key" ON "Artist"("serverId", "remoteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Song_serverId_remoteId_key" ON "Song"("serverId", "remoteId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_HistoryToSong_AB_unique" ON "_HistoryToSong"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_HistoryToSong_B_index" ON "_HistoryToSong"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_FolderToSong_AB_unique" ON "_FolderToSong"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_FolderToSong_B_index" ON "_FolderToSong"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_FolderToServerFolder_AB_unique" ON "_FolderToServerFolder"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_FolderToServerFolder_B_index" ON "_FolderToServerFolder"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ServerFolderToSong_AB_unique" ON "_ServerFolderToSong"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ServerFolderToSong_B_index" ON "_ServerFolderToSong"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_GenreToSong_AB_unique" ON "_GenreToSong"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_GenreToSong_B_index" ON "_GenreToSong"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ImageToSong_AB_unique" ON "_ImageToSong"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ImageToSong_B_index" ON "_ImageToSong"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ExternalToSong_AB_unique" ON "_ExternalToSong"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ExternalToSong_B_index" ON "_ExternalToSong"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumArtistToGenre_AB_unique" ON "_AlbumArtistToGenre"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumArtistToGenre_B_index" ON "_AlbumArtistToGenre"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumArtistToExternal_AB_unique" ON "_AlbumArtistToExternal"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumArtistToExternal_B_index" ON "_AlbumArtistToExternal"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumArtistToServerFolder_AB_unique" ON "_AlbumArtistToServerFolder"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumArtistToServerFolder_B_index" ON "_AlbumArtistToServerFolder"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumArtistToImage_AB_unique" ON "_AlbumArtistToImage"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumArtistToImage_B_index" ON "_AlbumArtistToImage"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumToGenre_AB_unique" ON "_AlbumToGenre"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumToGenre_B_index" ON "_AlbumToGenre"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumToArtist_AB_unique" ON "_AlbumToArtist"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumToArtist_B_index" ON "_AlbumToArtist"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumToAlbumArtist_AB_unique" ON "_AlbumToAlbumArtist"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumToAlbumArtist_B_index" ON "_AlbumToAlbumArtist"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumToExternal_AB_unique" ON "_AlbumToExternal"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumToExternal_B_index" ON "_AlbumToExternal"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumToServerFolder_AB_unique" ON "_AlbumToServerFolder"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumToServerFolder_B_index" ON "_AlbumToServerFolder"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_AlbumToImage_AB_unique" ON "_AlbumToImage"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_AlbumToImage_B_index" ON "_AlbumToImage"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ArtistToGenre_AB_unique" ON "_ArtistToGenre"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ArtistToGenre_B_index" ON "_ArtistToGenre"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ArtistToSong_AB_unique" ON "_ArtistToSong"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ArtistToSong_B_index" ON "_ArtistToSong"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ArtistToExternal_AB_unique" ON "_ArtistToExternal"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ArtistToExternal_B_index" ON "_ArtistToExternal"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ArtistToServerFolder_AB_unique" ON "_ArtistToServerFolder"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ArtistToServerFolder_B_index" ON "_ArtistToServerFolder"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_ArtistToImage_AB_unique" ON "_ArtistToImage"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_ArtistToImage_B_index" ON "_ArtistToImage"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "History" ADD CONSTRAINT "History_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ServerPermission" ADD CONSTRAINT "ServerPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ServerPermission" ADD CONSTRAINT "ServerPermission_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ServerUrl" ADD CONSTRAINT "ServerUrl_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserServerUrl" ADD CONSTRAINT "UserServerUrl_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserServerUrl" ADD CONSTRAINT "UserServerUrl_serverUrlId_fkey" FOREIGN KEY ("serverUrlId") REFERENCES "ServerUrl"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UserServerUrl" ADD CONSTRAINT "UserServerUrl_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ServerFolder" ADD CONSTRAINT "ServerFolder_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ServerFolderPermission" ADD CONSTRAINT "ServerFolderPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ServerFolderPermission" ADD CONSTRAINT "ServerFolderPermission_serverFolderId_fkey" FOREIGN KEY ("serverFolderId") REFERENCES "ServerFolder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumArtistFavorite" ADD CONSTRAINT "AlbumArtistFavorite_albumArtistId_fkey" FOREIGN KEY ("albumArtistId") REFERENCES "AlbumArtist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumArtistFavorite" ADD CONSTRAINT "AlbumArtistFavorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ArtistFavorite" ADD CONSTRAINT "ArtistFavorite_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ArtistFavorite" ADD CONSTRAINT "ArtistFavorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumFavorite" ADD CONSTRAINT "AlbumFavorite_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumFavorite" ADD CONSTRAINT "AlbumFavorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SongFavorite" ADD CONSTRAINT "SongFavorite_songId_fkey" FOREIGN KEY ("songId") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SongFavorite" ADD CONSTRAINT "SongFavorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumArtistRating" ADD CONSTRAINT "AlbumArtistRating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumArtistRating" ADD CONSTRAINT "AlbumArtistRating_albumArtistId_fkey" FOREIGN KEY ("albumArtistId") REFERENCES "AlbumArtist"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ArtistRating" ADD CONSTRAINT "ArtistRating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ArtistRating" ADD CONSTRAINT "ArtistRating_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "Artist"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumRating" ADD CONSTRAINT "AlbumRating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumRating" ADD CONSTRAINT "AlbumRating_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SongRating" ADD CONSTRAINT "SongRating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SongRating" ADD CONSTRAINT "SongRating_songId_fkey" FOREIGN KEY ("songId") REFERENCES "Song"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AlbumArtist" ADD CONSTRAINT "AlbumArtist_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Album" ADD CONSTRAINT "Album_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Artist" ADD CONSTRAINT "Artist_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_albumArtistId_fkey" FOREIGN KEY ("albumArtistId") REFERENCES "AlbumArtist"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Song" ADD CONSTRAINT "Song_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Task" ADD CONSTRAINT "Task_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_HistoryToSong" ADD CONSTRAINT "_HistoryToSong_A_fkey" FOREIGN KEY ("A") REFERENCES "History"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_HistoryToSong" ADD CONSTRAINT "_HistoryToSong_B_fkey" FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_FolderToSong" ADD CONSTRAINT "_FolderToSong_A_fkey" FOREIGN KEY ("A") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_FolderToSong" ADD CONSTRAINT "_FolderToSong_B_fkey" FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_FolderToServerFolder" ADD CONSTRAINT "_FolderToServerFolder_A_fkey" FOREIGN KEY ("A") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_FolderToServerFolder" ADD CONSTRAINT "_FolderToServerFolder_B_fkey" FOREIGN KEY ("B") REFERENCES "ServerFolder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ServerFolderToSong" ADD CONSTRAINT "_ServerFolderToSong_A_fkey" FOREIGN KEY ("A") REFERENCES "ServerFolder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ServerFolderToSong" ADD CONSTRAINT "_ServerFolderToSong_B_fkey" FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_GenreToSong" ADD CONSTRAINT "_GenreToSong_A_fkey" FOREIGN KEY ("A") REFERENCES "Genre"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_GenreToSong" ADD CONSTRAINT "_GenreToSong_B_fkey" FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ImageToSong" ADD CONSTRAINT "_ImageToSong_A_fkey" FOREIGN KEY ("A") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ImageToSong" ADD CONSTRAINT "_ImageToSong_B_fkey" FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ExternalToSong" ADD CONSTRAINT "_ExternalToSong_A_fkey" FOREIGN KEY ("A") REFERENCES "External"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ExternalToSong" ADD CONSTRAINT "_ExternalToSong_B_fkey" FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToGenre" ADD CONSTRAINT "_AlbumArtistToGenre_A_fkey" FOREIGN KEY ("A") REFERENCES "AlbumArtist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToGenre" ADD CONSTRAINT "_AlbumArtistToGenre_B_fkey" FOREIGN KEY ("B") REFERENCES "Genre"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToExternal" ADD CONSTRAINT "_AlbumArtistToExternal_A_fkey" FOREIGN KEY ("A") REFERENCES "AlbumArtist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToExternal" ADD CONSTRAINT "_AlbumArtistToExternal_B_fkey" FOREIGN KEY ("B") REFERENCES "External"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToServerFolder" ADD CONSTRAINT "_AlbumArtistToServerFolder_A_fkey" FOREIGN KEY ("A") REFERENCES "AlbumArtist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToServerFolder" ADD CONSTRAINT "_AlbumArtistToServerFolder_B_fkey" FOREIGN KEY ("B") REFERENCES "ServerFolder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToImage" ADD CONSTRAINT "_AlbumArtistToImage_A_fkey" FOREIGN KEY ("A") REFERENCES "AlbumArtist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumArtistToImage" ADD CONSTRAINT "_AlbumArtistToImage_B_fkey" FOREIGN KEY ("B") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToGenre" ADD CONSTRAINT "_AlbumToGenre_A_fkey" FOREIGN KEY ("A") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToGenre" ADD CONSTRAINT "_AlbumToGenre_B_fkey" FOREIGN KEY ("B") REFERENCES "Genre"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToArtist" ADD CONSTRAINT "_AlbumToArtist_A_fkey" FOREIGN KEY ("A") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToArtist" ADD CONSTRAINT "_AlbumToArtist_B_fkey" FOREIGN KEY ("B") REFERENCES "Artist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToAlbumArtist" ADD CONSTRAINT "_AlbumToAlbumArtist_A_fkey" FOREIGN KEY ("A") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToAlbumArtist" ADD CONSTRAINT "_AlbumToAlbumArtist_B_fkey" FOREIGN KEY ("B") REFERENCES "AlbumArtist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToExternal" ADD CONSTRAINT "_AlbumToExternal_A_fkey" FOREIGN KEY ("A") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToExternal" ADD CONSTRAINT "_AlbumToExternal_B_fkey" FOREIGN KEY ("B") REFERENCES "External"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToServerFolder" ADD CONSTRAINT "_AlbumToServerFolder_A_fkey" FOREIGN KEY ("A") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToServerFolder" ADD CONSTRAINT "_AlbumToServerFolder_B_fkey" FOREIGN KEY ("B") REFERENCES "ServerFolder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToImage" ADD CONSTRAINT "_AlbumToImage_A_fkey" FOREIGN KEY ("A") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_AlbumToImage" ADD CONSTRAINT "_AlbumToImage_B_fkey" FOREIGN KEY ("B") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToGenre" ADD CONSTRAINT "_ArtistToGenre_A_fkey" FOREIGN KEY ("A") REFERENCES "Artist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToGenre" ADD CONSTRAINT "_ArtistToGenre_B_fkey" FOREIGN KEY ("B") REFERENCES "Genre"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToSong" ADD CONSTRAINT "_ArtistToSong_A_fkey" FOREIGN KEY ("A") REFERENCES "Artist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToSong" ADD CONSTRAINT "_ArtistToSong_B_fkey" FOREIGN KEY ("B") REFERENCES "Song"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToExternal" ADD CONSTRAINT "_ArtistToExternal_A_fkey" FOREIGN KEY ("A") REFERENCES "Artist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToExternal" ADD CONSTRAINT "_ArtistToExternal_B_fkey" FOREIGN KEY ("B") REFERENCES "External"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToServerFolder" ADD CONSTRAINT "_ArtistToServerFolder_A_fkey" FOREIGN KEY ("A") REFERENCES "Artist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToServerFolder" ADD CONSTRAINT "_ArtistToServerFolder_B_fkey" FOREIGN KEY ("B") REFERENCES "ServerFolder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToImage" ADD CONSTRAINT "_ArtistToImage_A_fkey" FOREIGN KEY ("A") REFERENCES "Artist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_ArtistToImage" ADD CONSTRAINT "_ArtistToImage_B_fkey" FOREIGN KEY ("B") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
@@ -1,531 +0,0 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["fullTextSearch", "orderByNulls", "filteredRelationCount", "fieldReference"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum ServerType {
|
||||
SUBSONIC
|
||||
JELLYFIN
|
||||
NAVIDROME
|
||||
}
|
||||
|
||||
enum ServerPermissionType {
|
||||
ADMIN
|
||||
EDITOR
|
||||
VIEWER
|
||||
}
|
||||
|
||||
enum ExternalSource {
|
||||
MUSICBRAINZ
|
||||
LASTFM
|
||||
THEAUDIODB
|
||||
SPOTIFY
|
||||
}
|
||||
|
||||
enum ExternalType {
|
||||
ID
|
||||
LINK
|
||||
}
|
||||
|
||||
enum ImageType {
|
||||
PRIMARY
|
||||
BACKDROP
|
||||
LOGO
|
||||
SCREENSHOT
|
||||
}
|
||||
|
||||
enum TaskType {
|
||||
FULL_SCAN
|
||||
QUICK_SCAN
|
||||
REFRESH
|
||||
SPOTIFY
|
||||
MUSICBRAINZ
|
||||
LASTFM
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
token String @unique
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
username String @unique
|
||||
password String
|
||||
enabled Boolean @default(false)
|
||||
isAdmin Boolean @default(false)
|
||||
deviceId String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
histories History[]
|
||||
albumArtistRatings AlbumArtistRating[]
|
||||
artistRatings ArtistRating[]
|
||||
albumRatings AlbumRating[]
|
||||
songRatings SongRating[]
|
||||
refreshTokens RefreshToken[]
|
||||
|
||||
serverFolderPermissions ServerFolderPermission[]
|
||||
serverPermissions ServerPermission[]
|
||||
// serverCredentials ServerCredential[]
|
||||
albumArtistFavorites AlbumArtistFavorite[]
|
||||
artistFavorites ArtistFavorite[]
|
||||
albumFavorites AlbumFavorite[]
|
||||
songFavorites SongFavorite[]
|
||||
userServerUrls UserServerUrl[]
|
||||
}
|
||||
|
||||
model History {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
|
||||
songs Song[]
|
||||
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
}
|
||||
|
||||
model Server {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
url String @unique
|
||||
remoteUserId String
|
||||
username String
|
||||
token String
|
||||
type ServerType
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
albumArtists AlbumArtist[]
|
||||
artists Artist[]
|
||||
albums Album[]
|
||||
songs Song[]
|
||||
serverFolders ServerFolder[]
|
||||
serverUrls ServerUrl[]
|
||||
folders Folder[]
|
||||
serverPermissions ServerPermission[]
|
||||
// serverCredentials ServerCredential[]
|
||||
tasks Task[]
|
||||
userServerUrls UserServerUrl[]
|
||||
}
|
||||
|
||||
// model ServerCredential {
|
||||
// id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
// username String
|
||||
// enabled Boolean @default(false)
|
||||
// credential String
|
||||
// createdAt DateTime @default(now())
|
||||
// updatedAt DateTime @updatedAt
|
||||
|
||||
// server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
// serverId String @db.Uuid
|
||||
|
||||
// user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
// userId String @db.Uuid
|
||||
// }
|
||||
|
||||
model Folder {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
path String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
songs Song[]
|
||||
serverFolders ServerFolder[]
|
||||
|
||||
parentId String? @db.Uuid
|
||||
parent Folder? @relation("FolderChildren", fields: [parentId], references: [id])
|
||||
children Folder[] @relation("FolderChildren")
|
||||
|
||||
Server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [serverId, path], name: "uniqueFolderId")
|
||||
}
|
||||
|
||||
model ServerPermission {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
type ServerPermissionType
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, serverId], name: "uniqueServerPermissionsId")
|
||||
}
|
||||
|
||||
model ServerUrl {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
url String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
userServerUrls UserServerUrl[]
|
||||
|
||||
@@unique(fields: [serverId, url], name: "uniqueServerUrlId")
|
||||
}
|
||||
|
||||
model UserServerUrl {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
|
||||
serverUrl ServerUrl @relation(fields: [serverUrlId], references: [id], onDelete: Cascade)
|
||||
serverUrlId String @db.Uuid
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, serverId], name: "uniqueUserServerUrlId")
|
||||
}
|
||||
|
||||
model ServerFolder {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
remoteId String @unique
|
||||
enabled Boolean @default(true)
|
||||
lastScannedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deleted Boolean @default(false)
|
||||
|
||||
albumArtists AlbumArtist[]
|
||||
artists Artist[]
|
||||
albums Album[]
|
||||
songs Song[]
|
||||
folders Folder[]
|
||||
serverFolderPermissions ServerFolderPermission[]
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [serverId, remoteId], name: "uniqueServerFolderId")
|
||||
}
|
||||
|
||||
model ServerFolderPermission {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @db.Uuid
|
||||
|
||||
serverFolder ServerFolder @relation(fields: [serverFolderId], references: [id], onDelete: Cascade)
|
||||
serverFolderId String @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, serverFolderId], name: "uniqueServerFolderPermissionsId")
|
||||
}
|
||||
|
||||
model Genre {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
albumArtists AlbumArtist[]
|
||||
artists Artist[]
|
||||
albums Album[]
|
||||
songs Song[]
|
||||
}
|
||||
|
||||
model AlbumArtistFavorite {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
albumArtist AlbumArtist @relation(fields: [albumArtistId], references: [id], onDelete: Cascade)
|
||||
albumArtistId String @db.Uuid
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
|
||||
@@id([userId, albumArtistId])
|
||||
@@unique(fields: [userId, albumArtistId], name: "uniqueAlbumArtistFavoriteId")
|
||||
}
|
||||
|
||||
model ArtistFavorite {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
artist Artist @relation(fields: [artistId], references: [id], onDelete: Cascade)
|
||||
artistId String @db.Uuid
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
|
||||
@@id([userId, artistId])
|
||||
@@unique(fields: [userId, artistId], name: "uniqueArtistFavoriteId")
|
||||
}
|
||||
|
||||
model AlbumFavorite {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
album Album @relation(fields: [albumId], references: [id], onDelete: Cascade)
|
||||
albumId String @db.Uuid
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
|
||||
@@id([userId, albumId])
|
||||
@@unique(fields: [userId, albumId], name: "uniqueAlbumFavoriteId")
|
||||
}
|
||||
|
||||
model SongFavorite {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
song Song @relation(fields: [songId], references: [id], onDelete: Cascade)
|
||||
songId String @db.Uuid
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String @db.Uuid
|
||||
|
||||
@@id([userId, songId])
|
||||
@@unique(fields: [userId, songId], name: "uniqueSongFavoriteId")
|
||||
}
|
||||
|
||||
model AlbumArtistRating {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
value Float
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @db.Uuid
|
||||
|
||||
albumArtist AlbumArtist @relation(fields: [albumArtistId], references: [id])
|
||||
albumArtistId String @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, albumArtistId], name: "uniqueAlbumArtistRatingId")
|
||||
}
|
||||
|
||||
model ArtistRating {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
value Float
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @db.Uuid
|
||||
|
||||
artist Artist @relation(fields: [artistId], references: [id])
|
||||
artistId String @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, artistId], name: "uniqueArtistRatingId")
|
||||
}
|
||||
|
||||
model AlbumRating {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
value Float
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @db.Uuid
|
||||
|
||||
album Album @relation(fields: [albumId], references: [id])
|
||||
albumId String @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, albumId], name: "uniqueAlbumRatingId")
|
||||
}
|
||||
|
||||
model SongRating {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
value Float
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
userId String @db.Uuid
|
||||
|
||||
song Song @relation(fields: [songId], references: [id])
|
||||
songId String @db.Uuid
|
||||
|
||||
@@unique(fields: [userId, songId], name: "uniqueSongRatingId")
|
||||
}
|
||||
|
||||
model Image {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
url String?
|
||||
remoteUrl String
|
||||
type ImageType
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
albumArtists AlbumArtist[]
|
||||
artists Artist[]
|
||||
albums Album[]
|
||||
songs Song[]
|
||||
|
||||
@@unique(fields: [remoteUrl, type], name: "uniqueImageId")
|
||||
}
|
||||
|
||||
model External {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
value String
|
||||
type ExternalType
|
||||
source ExternalSource
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
albumArtists AlbumArtist[]
|
||||
artists Artist[]
|
||||
albums Album[]
|
||||
songs Song[]
|
||||
|
||||
@@unique(fields: [value, source], name: "uniqueExternalId")
|
||||
}
|
||||
|
||||
model AlbumArtist {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
sortName String
|
||||
biography String?
|
||||
remoteId String
|
||||
remoteCreatedAt DateTime?
|
||||
deleted Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
albums Album[]
|
||||
genres Genre[]
|
||||
externals External[]
|
||||
serverFolders ServerFolder[]
|
||||
ratings AlbumArtistRating[]
|
||||
images Image[]
|
||||
songs Song[]
|
||||
albumArtistFavorites AlbumArtistFavorite[]
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [serverId, remoteId], name: "uniqueAlbumArtistId")
|
||||
}
|
||||
|
||||
model Album {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
sortName String
|
||||
releaseDate DateTime?
|
||||
releaseYear Int?
|
||||
remoteId String
|
||||
remoteCreatedAt DateTime?
|
||||
deleted Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
songs Song[]
|
||||
genres Genre[]
|
||||
artists Artist[]
|
||||
albumArtists AlbumArtist[]
|
||||
externals External[]
|
||||
serverFolders ServerFolder[]
|
||||
ratings AlbumRating[]
|
||||
images Image[]
|
||||
favorites AlbumFavorite[]
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [serverId, remoteId], name: "uniqueAlbumId")
|
||||
}
|
||||
|
||||
model Artist {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
sortName String
|
||||
biography String?
|
||||
remoteId String
|
||||
remoteCreatedAt DateTime?
|
||||
deleted Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
genres Genre[]
|
||||
albums Album[]
|
||||
songs Song[]
|
||||
externals External[]
|
||||
serverFolders ServerFolder[]
|
||||
ratings ArtistRating[]
|
||||
images Image[]
|
||||
favorites ArtistFavorite[]
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [serverId, remoteId], name: "uniqueArtistId")
|
||||
}
|
||||
|
||||
model Song {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
sortName String
|
||||
releaseDate DateTime?
|
||||
releaseYear Int?
|
||||
duration Float
|
||||
size Int?
|
||||
lyrics String?
|
||||
bitRate Int
|
||||
container String
|
||||
discNumber Int @default(1)
|
||||
trackNumber Int?
|
||||
artistName String?
|
||||
remoteId String
|
||||
remoteCreatedAt DateTime?
|
||||
deleted Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
genres Genre[]
|
||||
artists Artist[]
|
||||
externals External[]
|
||||
folders Folder[]
|
||||
serverFolders ServerFolder[]
|
||||
histories History[]
|
||||
ratings SongRating[]
|
||||
images Image[]
|
||||
favorites SongFavorite[]
|
||||
|
||||
albumArtist AlbumArtist? @relation(fields: [albumArtistId], references: [id])
|
||||
albumArtistId String? @db.Uuid
|
||||
|
||||
album Album? @relation(fields: [albumId], references: [id])
|
||||
albumId String? @db.Uuid
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
|
||||
@@unique(fields: [serverId, remoteId], name: "uniqueSongId")
|
||||
}
|
||||
|
||||
model Task {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String
|
||||
type TaskType
|
||||
message String?
|
||||
progress String?
|
||||
completed Boolean @default(false)
|
||||
isError Boolean? @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
|
||||
serverId String @db.Uuid
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/* eslint-disable promise/catch-or-return */
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { randomString } from '../utils';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const hashedPassword = await bcrypt.hash('admin', 12);
|
||||
|
||||
await prisma.user.upsert({
|
||||
create: {
|
||||
deviceId: `admin_${randomString(10)}`,
|
||||
enabled: true,
|
||||
isAdmin: true,
|
||||
password: hashedPassword,
|
||||
username: 'admin',
|
||||
},
|
||||
update: {},
|
||||
where: { username: 'admin' },
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
// process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './subsonic';
|
||||
export * from './jellyfin';
|
||||
@@ -1,7 +0,0 @@
|
||||
import { jellyfinApi } from './jellyfin.api';
|
||||
import { jellyfinScanner } from './jellyfin.scanner';
|
||||
|
||||
export const jellyfin = {
|
||||
api: jellyfinApi,
|
||||
scanner: jellyfinScanner,
|
||||
};
|
||||
@@ -1,117 +0,0 @@
|
||||
import { Server } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
JFAlbumArtistsResponse,
|
||||
JFAlbumsResponse,
|
||||
JFArtistsResponse,
|
||||
JFAuthenticate,
|
||||
JFCollectionType,
|
||||
JFGenreResponse,
|
||||
JFItemType,
|
||||
JFMusicFoldersResponse,
|
||||
JFRequestParams,
|
||||
JFSongsResponse,
|
||||
} from './jellyfin.types';
|
||||
|
||||
export const api = axios.create({});
|
||||
|
||||
export const authenticate = async (options: {
|
||||
password: string;
|
||||
url: string;
|
||||
username: string;
|
||||
}) => {
|
||||
const { password, url, username } = options;
|
||||
const cleanServerUrl = url.replace(/\/$/, '');
|
||||
|
||||
const { data } = await api.post<JFAuthenticate>(
|
||||
`${cleanServerUrl}/users/authenticatebyname`,
|
||||
{ pw: password, username },
|
||||
{
|
||||
headers: {
|
||||
'X-Emby-Authorization': `MediaBrowser Client="Sonixd", Device="PC", DeviceId="Sonixd", Version="1.0.0-alpha1"`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getMusicFolders = async (server: Partial<Server>) => {
|
||||
const { data } = await api.get<JFMusicFoldersResponse>(
|
||||
`${server.url}/users/${server.remoteUserId}/items`,
|
||||
{ headers: { 'X-MediaBrowser-Token': server.token! } }
|
||||
);
|
||||
|
||||
const musicFolders = data.Items.filter(
|
||||
(folder) => folder.CollectionType === JFCollectionType.MUSIC
|
||||
);
|
||||
|
||||
return musicFolders;
|
||||
};
|
||||
|
||||
export const getGenres = async (server: Server, params: JFRequestParams) => {
|
||||
const { data } = await api.get<JFGenreResponse>(`${server.url}/genres`, {
|
||||
headers: { 'X-MediaBrowser-Token': server.token },
|
||||
params,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getAlbumArtists = async (
|
||||
server: Server,
|
||||
params: JFRequestParams
|
||||
) => {
|
||||
const { data } = await api.get<JFAlbumArtistsResponse>(
|
||||
`${server.url}/artists/albumArtists`,
|
||||
{
|
||||
headers: { 'X-MediaBrowser-Token': server.token },
|
||||
params,
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getArtists = async (server: Server, params: JFRequestParams) => {
|
||||
const { data } = await api.get<JFArtistsResponse>(`${server.url}/artists`, {
|
||||
headers: { 'X-MediaBrowser-Token': server.token },
|
||||
params,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getAlbums = async (server: Server, params: JFRequestParams) => {
|
||||
const { data } = await api.get<JFAlbumsResponse>(
|
||||
`${server.url}/users/${server.remoteUserId}/items`,
|
||||
{
|
||||
headers: { 'X-MediaBrowser-Token': server.token },
|
||||
params: { includeItemTypes: JFItemType.MUSICALBUM, ...params },
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getSongs = async (server: Server, params: JFRequestParams) => {
|
||||
const { data } = await api.get<JFSongsResponse>(
|
||||
`${server.url}/users/${server.remoteUserId}/items`,
|
||||
{
|
||||
headers: { 'X-MediaBrowser-Token': server.token },
|
||||
params: { includeItemTypes: JFItemType.AUDIO, ...params },
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const jellyfinApi = {
|
||||
authenticate,
|
||||
getAlbumArtists,
|
||||
getAlbums,
|
||||
getArtists,
|
||||
getGenres,
|
||||
getMusicFolders,
|
||||
getSongs,
|
||||
};
|
||||
@@ -1,495 +0,0 @@
|
||||
import {
|
||||
ExternalSource,
|
||||
Folder,
|
||||
ImageType,
|
||||
Server,
|
||||
ServerFolder,
|
||||
Task,
|
||||
} from '@prisma/client';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { prisma } from '../../lib';
|
||||
import { groupByProperty } from '../../utils';
|
||||
import { queue } from '../queues';
|
||||
import { jellyfinApi } from './jellyfin.api';
|
||||
import { JFExternalType, JFImageType, JFItemType } from './jellyfin.types';
|
||||
import { jellyfinUtils } from './jellyfin.utils';
|
||||
|
||||
const scanGenres = async (options: {
|
||||
server: Server;
|
||||
serverFolder: ServerFolder;
|
||||
task: Task;
|
||||
}) => {
|
||||
await prisma.task.update({
|
||||
data: { message: 'Scanning genres' },
|
||||
where: { id: options.task.id },
|
||||
});
|
||||
|
||||
const genres = await jellyfinApi.getGenres(options.server, {
|
||||
parentId: options.serverFolder.remoteId,
|
||||
});
|
||||
|
||||
const genresCreate = genres.Items.map((genre) => {
|
||||
return { name: genre.Name };
|
||||
});
|
||||
|
||||
await prisma.genre.createMany({
|
||||
data: genresCreate,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
const scanAlbumArtists = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder,
|
||||
task: Task
|
||||
) => {
|
||||
await prisma.task.update({
|
||||
data: { message: 'Scanning album artists' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
// TODO: Possibly need to scan without the parentId to get all artists, since Jellyfin may link an album to an artist of a different folder
|
||||
const albumArtists = await jellyfinApi.getAlbumArtists(server, {
|
||||
fields: 'Genres,DateCreated,ExternalUrls,Overview',
|
||||
parentId: serverFolder.remoteId,
|
||||
});
|
||||
|
||||
await jellyfinUtils.insertGenres(albumArtists.Items);
|
||||
await jellyfinUtils.insertImages(albumArtists.Items);
|
||||
await jellyfinUtils.insertExternals(albumArtists.Items);
|
||||
|
||||
for (const albumArtist of albumArtists.Items) {
|
||||
const genresConnect = albumArtist.Genres.map((genre) => ({ name: genre }));
|
||||
|
||||
const imagesConnectOrCreate = [];
|
||||
for (const backdrop of albumArtist.BackdropImageTags) {
|
||||
imagesConnectOrCreate.push({
|
||||
create: { remoteUrl: backdrop, type: ImageType.BACKDROP },
|
||||
where: {
|
||||
uniqueImageId: { remoteUrl: backdrop, type: ImageType.BACKDROP },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(albumArtist.ImageTags)) {
|
||||
if (key === JFImageType.PRIMARY) {
|
||||
imagesConnectOrCreate.push({
|
||||
create: { remoteUrl: value, type: ImageType.PRIMARY },
|
||||
where: {
|
||||
uniqueImageId: { remoteUrl: value, type: ImageType.PRIMARY },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (key === JFImageType.LOGO) {
|
||||
imagesConnectOrCreate.push({
|
||||
create: { remoteUrl: value, type: ImageType.LOGO },
|
||||
where: {
|
||||
uniqueImageId: { remoteUrl: value, type: ImageType.LOGO },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const externalsConnect = albumArtist.ExternalUrls.map((external) => ({
|
||||
uniqueExternalId: {
|
||||
source:
|
||||
external.Name === JFExternalType.MUSICBRAINZ
|
||||
? ExternalSource.MUSICBRAINZ
|
||||
: ExternalSource.THEAUDIODB,
|
||||
value: external.Url.split('/').pop() || '',
|
||||
},
|
||||
}));
|
||||
|
||||
await prisma.albumArtist.upsert({
|
||||
create: {
|
||||
biography: albumArtist.Overview,
|
||||
externals: { connect: externalsConnect },
|
||||
genres: { connect: genresConnect },
|
||||
images: {
|
||||
connectOrCreate: imagesConnectOrCreate,
|
||||
},
|
||||
name: albumArtist.Name,
|
||||
remoteCreatedAt: albumArtist.DateCreated,
|
||||
remoteId: albumArtist.Id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: albumArtist.Name,
|
||||
},
|
||||
update: {
|
||||
biography: albumArtist.Overview,
|
||||
deleted: false,
|
||||
externals: { connect: externalsConnect },
|
||||
genres: { connect: genresConnect },
|
||||
images: {
|
||||
connectOrCreate: imagesConnectOrCreate,
|
||||
},
|
||||
name: albumArtist.Name,
|
||||
remoteCreatedAt: albumArtist.DateCreated,
|
||||
remoteId: albumArtist.Id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: albumArtist.Name,
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: albumArtist.Id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const scanAlbums = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder,
|
||||
task: Task
|
||||
) => {
|
||||
const check = await jellyfinApi.getAlbums(server, {
|
||||
enableUserData: false,
|
||||
includeItemTypes: JFItemType.MUSICALBUM,
|
||||
limit: 1,
|
||||
parentId: serverFolder.remoteId,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const albumCount = check.TotalRecordCount;
|
||||
const chunkSize = 5000;
|
||||
const albumChunkCount = Math.ceil(albumCount / chunkSize);
|
||||
|
||||
await prisma.task.update({
|
||||
data: { message: 'Scanning albums' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
for (let i = 0; i < albumChunkCount; i += 1) {
|
||||
const albums = await jellyfinApi.getAlbums(server, {
|
||||
enableImageTypes: 'Primary,Logo,Backdrop',
|
||||
enableUserData: false,
|
||||
fields: 'Genres,DateCreated,ExternalUrls,Overview',
|
||||
imageTypeLimit: 1,
|
||||
limit: chunkSize,
|
||||
parentId: serverFolder.remoteId,
|
||||
recursive: true,
|
||||
startIndex: i * chunkSize,
|
||||
});
|
||||
|
||||
await jellyfinUtils.insertGenres(albums.Items);
|
||||
await jellyfinUtils.insertImages(albums.Items);
|
||||
await jellyfinUtils.insertExternals(albums.Items);
|
||||
|
||||
for (const album of albums.Items) {
|
||||
const genresConnect = album.Genres.map((genre) => ({ name: genre }));
|
||||
|
||||
const imagesConnectOrCreate = [];
|
||||
for (const [key, value] of Object.entries(album.ImageTags)) {
|
||||
if (key === JFImageType.PRIMARY) {
|
||||
imagesConnectOrCreate.push({
|
||||
create: { remoteUrl: value, type: ImageType.PRIMARY },
|
||||
where: {
|
||||
uniqueImageId: { remoteUrl: value, type: ImageType.PRIMARY },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (key === JFImageType.LOGO) {
|
||||
imagesConnectOrCreate.push({
|
||||
create: { remoteUrl: value, type: ImageType.LOGO },
|
||||
where: {
|
||||
uniqueImageId: { remoteUrl: value, type: ImageType.LOGO },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const externalsConnect = album.ExternalUrls.map((external) => ({
|
||||
uniqueExternalId: {
|
||||
source:
|
||||
external.Name === JFExternalType.MUSICBRAINZ
|
||||
? ExternalSource.MUSICBRAINZ
|
||||
: ExternalSource.THEAUDIODB,
|
||||
value: external.Url.split('/').pop() || '',
|
||||
},
|
||||
}));
|
||||
|
||||
const remoteAlbumArtists = album.AlbumArtists;
|
||||
|
||||
const albumArtists = await prisma.albumArtist.findMany({
|
||||
where: {
|
||||
remoteId: { in: remoteAlbumArtists.map((artist) => artist.Id) },
|
||||
},
|
||||
});
|
||||
|
||||
const albumArtistsConnect = [];
|
||||
for (const albumArtist of remoteAlbumArtists) {
|
||||
const invalid = !albumArtists.find(
|
||||
(artist) => artist.remoteId === albumArtist.Id
|
||||
);
|
||||
|
||||
if (invalid) {
|
||||
// If Jellyfin returns an invalid album artist, we'll just use the first matching one
|
||||
const foundAlternate = await prisma.albumArtist.findFirst({
|
||||
where: {
|
||||
name: albumArtist.Name,
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (foundAlternate) {
|
||||
albumArtistsConnect.push({
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: foundAlternate.remoteId,
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
albumArtistsConnect.push({
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: albumArtist.Id,
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.album.upsert({
|
||||
create: {
|
||||
albumArtists: { connect: albumArtistsConnect },
|
||||
externals: { connect: externalsConnect },
|
||||
genres: { connect: genresConnect },
|
||||
images: { connectOrCreate: imagesConnectOrCreate },
|
||||
name: album.Name,
|
||||
releaseDate: album.PremiereDate,
|
||||
releaseYear: album.ProductionYear,
|
||||
remoteCreatedAt: album.DateCreated,
|
||||
remoteId: album.Id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: album.Name,
|
||||
},
|
||||
update: {
|
||||
albumArtists: { connect: albumArtistsConnect },
|
||||
deleted: false,
|
||||
externals: { connect: externalsConnect },
|
||||
genres: { connect: genresConnect },
|
||||
images: { connectOrCreate: imagesConnectOrCreate },
|
||||
name: album.Name,
|
||||
releaseDate: album.PremiereDate,
|
||||
releaseYear: album.ProductionYear,
|
||||
remoteCreatedAt: album.DateCreated,
|
||||
remoteId: album.Id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: album.Name,
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumId: {
|
||||
remoteId: album.Id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const scanSongs = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder,
|
||||
task: Task
|
||||
) => {
|
||||
const check = await jellyfinApi.getSongs(server, {
|
||||
enableUserData: false,
|
||||
limit: 0,
|
||||
parentId: serverFolder.remoteId,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
const songCount = check.TotalRecordCount;
|
||||
const chunkSize = 5000;
|
||||
const songChunkCount = Math.ceil(songCount / chunkSize);
|
||||
|
||||
await prisma.task.update({
|
||||
data: { message: 'Scanning songs' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
for (let i = 0; i < songChunkCount; i += 1) {
|
||||
const songs = await jellyfinApi.getSongs(server, {
|
||||
enableImageTypes: 'Primary,Logo,Backdrop',
|
||||
enableUserData: false,
|
||||
fields: 'Genres,DateCreated,ExternalUrls,MediaSources,SortName',
|
||||
imageTypeLimit: 1,
|
||||
limit: chunkSize,
|
||||
parentId: serverFolder.remoteId,
|
||||
recursive: true,
|
||||
sortBy: 'DateCreated,Album',
|
||||
sortOrder: 'Descending',
|
||||
startIndex: i * chunkSize,
|
||||
});
|
||||
|
||||
const folderGroups = songs.Items.map((song) => {
|
||||
const songPaths = song.MediaSources[0].Path.split('/');
|
||||
const paths = [];
|
||||
for (let b = 0; b < songPaths.length - 1; b += 1) {
|
||||
paths.push({
|
||||
name: songPaths[b],
|
||||
path: songPaths.slice(0, b + 1).join('/'),
|
||||
});
|
||||
}
|
||||
|
||||
return paths;
|
||||
});
|
||||
|
||||
const uniqueFolders = uniqBy(
|
||||
folderGroups.flatMap((folder) => folder).filter((f) => f.path !== ''),
|
||||
'path'
|
||||
);
|
||||
|
||||
const createdFolders: Folder[] = [];
|
||||
for (const folder of uniqueFolders) {
|
||||
const createdFolder = await prisma.folder.upsert({
|
||||
create: {
|
||||
name: folder.name,
|
||||
path: folder.path,
|
||||
serverFolders: {
|
||||
connect: {
|
||||
uniqueServerFolderId: {
|
||||
remoteId: serverFolder.remoteId,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
serverId: server.id,
|
||||
},
|
||||
update: {
|
||||
name: folder.name,
|
||||
path: folder.path,
|
||||
serverFolders: {
|
||||
connect: {
|
||||
uniqueServerFolderId: {
|
||||
remoteId: serverFolder.remoteId,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
uniqueFolderId: {
|
||||
path: folder.path,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createdFolders.push(createdFolder);
|
||||
}
|
||||
|
||||
for (const folder of createdFolders) {
|
||||
if (folder.parentId) break;
|
||||
|
||||
const pathSplit = folder.path.split('/');
|
||||
const parentPath = pathSplit.slice(0, pathSplit.length - 1).join('/');
|
||||
|
||||
const parentPathData = createdFolders.find(
|
||||
(save) => save.path === parentPath
|
||||
);
|
||||
|
||||
if (parentPathData) {
|
||||
await prisma.folder.update({
|
||||
data: {
|
||||
parentId: parentPathData.id,
|
||||
},
|
||||
where: { id: folder.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await jellyfinUtils.insertArtists(server, serverFolder, songs.Items);
|
||||
await jellyfinUtils.insertImages(songs.Items);
|
||||
await jellyfinUtils.insertExternals(songs.Items);
|
||||
|
||||
const albumSongGroups = groupByProperty(songs.Items, 'AlbumId');
|
||||
const keys = Object.keys(albumSongGroups);
|
||||
|
||||
for (const key of keys) {
|
||||
const songGroup = albumSongGroups[key];
|
||||
await jellyfinUtils.insertSongGroup(server, serverFolder, songGroup, key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkDeleted = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder,
|
||||
task: Task
|
||||
) => {
|
||||
await prisma.$transaction([
|
||||
prisma.albumArtist.updateMany({
|
||||
data: { deleted: true },
|
||||
where: {
|
||||
serverFolders: { some: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
updatedAt: { lte: task.createdAt },
|
||||
},
|
||||
}),
|
||||
prisma.artist.updateMany({
|
||||
data: { deleted: true },
|
||||
where: {
|
||||
serverFolders: { some: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
updatedAt: { lte: task.createdAt },
|
||||
},
|
||||
}),
|
||||
prisma.album.updateMany({
|
||||
data: { deleted: true },
|
||||
where: {
|
||||
serverFolders: { some: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
updatedAt: { lte: task.createdAt },
|
||||
},
|
||||
}),
|
||||
prisma.song.updateMany({
|
||||
data: { deleted: true },
|
||||
where: {
|
||||
serverFolders: { some: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
updatedAt: { lte: task.createdAt },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
const scanAll = async (
|
||||
server: Server,
|
||||
serverFolders: ServerFolder[],
|
||||
task: Task
|
||||
) => {
|
||||
queue.scanner.push({
|
||||
fn: async () => {
|
||||
await prisma.task.update({
|
||||
data: { message: 'Beginning scan...' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
for (const serverFolder of serverFolders) {
|
||||
await scanGenres({ server, serverFolder, task });
|
||||
await scanAlbumArtists(server, serverFolder, task);
|
||||
await scanAlbums(server, serverFolder, task);
|
||||
await scanSongs(server, serverFolder, task);
|
||||
await checkDeleted(server, serverFolder, task);
|
||||
}
|
||||
|
||||
return { task };
|
||||
},
|
||||
id: task.id,
|
||||
});
|
||||
};
|
||||
|
||||
export const jellyfinScanner = {
|
||||
scanAlbumArtists,
|
||||
scanAlbums,
|
||||
scanAll,
|
||||
scanGenres,
|
||||
scanSongs,
|
||||
};
|
||||
@@ -1,404 +0,0 @@
|
||||
export interface JFBaseResponse {
|
||||
StartIndex: number;
|
||||
TotalRecordCount: number;
|
||||
}
|
||||
|
||||
export interface JFMusicFoldersResponse extends JFBaseResponse {
|
||||
Items: JFMusicFolder[];
|
||||
}
|
||||
|
||||
export interface JFGenreResponse extends JFBaseResponse {
|
||||
Items: JFGenre[];
|
||||
}
|
||||
|
||||
export interface JFAlbumArtistsResponse extends JFBaseResponse {
|
||||
Items: JFAlbumArtist[];
|
||||
}
|
||||
|
||||
export interface JFArtistsResponse extends JFBaseResponse {
|
||||
Items: JFAlbumArtist[];
|
||||
}
|
||||
|
||||
export interface JFAlbumsResponse extends JFBaseResponse {
|
||||
Items: JFAlbum[];
|
||||
}
|
||||
|
||||
export interface JFSongsResponse extends JFBaseResponse {
|
||||
Items: JFSong[];
|
||||
}
|
||||
|
||||
export interface JFRequestParams {
|
||||
albumArtistIds?: string;
|
||||
artistIds?: string;
|
||||
enableImageTypes?: string;
|
||||
enableTotalRecordCount?: boolean;
|
||||
enableUserData?: boolean;
|
||||
excludeItemTypes?: string;
|
||||
fields?: string;
|
||||
imageTypeLimit?: number;
|
||||
includeItemTypes?: string;
|
||||
isFavorite?: boolean;
|
||||
limit?: number;
|
||||
parentId?: string;
|
||||
recursive?: boolean;
|
||||
searchTerm?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'Ascending' | 'Descending';
|
||||
startIndex?: number;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface JFMusicFolder {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
CollectionType: string;
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
UserData: UserData;
|
||||
}
|
||||
|
||||
export interface JFGenre {
|
||||
BackdropImageTags: any[];
|
||||
ChannelId: null;
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: ImageTags;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
export interface JFAlbumArtist {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: ImageTags;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
export interface JFArtist {
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: GenreItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: any;
|
||||
ImageTags: string[];
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
Overview?: string;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
export interface JFAlbum {
|
||||
AlbumArtist: string;
|
||||
AlbumArtists: JFGenericItem[];
|
||||
ArtistItems: JFGenericItem[];
|
||||
Artists: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: JFGenericItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
Name: string;
|
||||
ParentLogoImageTag: string;
|
||||
ParentLogoItemId: string;
|
||||
PremiereDate?: string;
|
||||
ProductionYear: number;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
export interface JFSong {
|
||||
Album: string;
|
||||
AlbumArtist: string;
|
||||
AlbumArtists: JFGenericItem[];
|
||||
AlbumId: string;
|
||||
AlbumPrimaryImageTag: string;
|
||||
ArtistItems: JFGenericItem[];
|
||||
Artists: string[];
|
||||
BackdropImageTags: string[];
|
||||
ChannelId: null;
|
||||
DateCreated: string;
|
||||
ExternalUrls: ExternalURL[];
|
||||
GenreItems: JFGenericItem[];
|
||||
Genres: string[];
|
||||
Id: string;
|
||||
ImageBlurHashes: ImageBlurHashes;
|
||||
ImageTags: ImageTags;
|
||||
IndexNumber: number;
|
||||
IsFolder: boolean;
|
||||
LocationType: string;
|
||||
MediaSources: MediaSources[];
|
||||
MediaType: string;
|
||||
Name: string;
|
||||
ParentIndexNumber: number;
|
||||
PremiereDate?: string;
|
||||
ProductionYear: number;
|
||||
RunTimeTicks: number;
|
||||
ServerId: string;
|
||||
SortName: string;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
interface ImageBlurHashes {
|
||||
Backdrop?: any;
|
||||
Logo?: any;
|
||||
Primary?: any;
|
||||
}
|
||||
|
||||
interface ImageTags {
|
||||
Logo?: string;
|
||||
Primary?: string;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
IsFavorite: boolean;
|
||||
Key: string;
|
||||
PlayCount: number;
|
||||
PlaybackPositionTicks: number;
|
||||
Played: boolean;
|
||||
}
|
||||
|
||||
interface ExternalURL {
|
||||
Name: string;
|
||||
Url: string;
|
||||
}
|
||||
|
||||
interface GenreItem {
|
||||
Id: string;
|
||||
Name: string;
|
||||
}
|
||||
|
||||
export interface JFGenericItem {
|
||||
Id: string;
|
||||
Name: string;
|
||||
}
|
||||
|
||||
interface MediaSources {
|
||||
Bitrate: number;
|
||||
Container: string;
|
||||
DefaultAudioStreamIndex: number;
|
||||
ETag: string;
|
||||
Formats: any[];
|
||||
GenPtsInput: boolean;
|
||||
Id: string;
|
||||
IgnoreDts: boolean;
|
||||
IgnoreIndex: boolean;
|
||||
IsInfiniteStream: boolean;
|
||||
IsRemote: boolean;
|
||||
MediaAttachments: any[];
|
||||
MediaStreams: MediaStream[];
|
||||
Name: string;
|
||||
Path: string;
|
||||
Protocol: string;
|
||||
ReadAtNativeFramerate: boolean;
|
||||
RequiredHttpHeaders: any;
|
||||
RequiresClosing: boolean;
|
||||
RequiresLooping: boolean;
|
||||
RequiresOpening: boolean;
|
||||
RunTimeTicks: number;
|
||||
Size: number;
|
||||
SupportsDirectPlay: boolean;
|
||||
SupportsDirectStream: boolean;
|
||||
SupportsProbing: boolean;
|
||||
SupportsTranscoding: boolean;
|
||||
Type: string;
|
||||
}
|
||||
|
||||
interface MediaStream {
|
||||
AspectRatio?: string;
|
||||
BitDepth?: number;
|
||||
BitRate?: number;
|
||||
ChannelLayout?: string;
|
||||
Channels?: number;
|
||||
Codec: string;
|
||||
CodecTimeBase: string;
|
||||
ColorSpace?: string;
|
||||
Comment?: string;
|
||||
DisplayTitle?: string;
|
||||
Height?: number;
|
||||
Index: number;
|
||||
IsDefault: boolean;
|
||||
IsExternal: boolean;
|
||||
IsForced: boolean;
|
||||
IsInterlaced: boolean;
|
||||
IsTextSubtitleStream: boolean;
|
||||
Level: number;
|
||||
PixelFormat?: string;
|
||||
Profile?: string;
|
||||
RealFrameRate?: number;
|
||||
RefFrames?: number;
|
||||
SampleRate?: number;
|
||||
SupportsExternalStream: boolean;
|
||||
TimeBase: string;
|
||||
Type: string;
|
||||
Width?: number;
|
||||
}
|
||||
|
||||
export enum JFExternalType {
|
||||
MUSICBRAINZ = 'MusicBrainz',
|
||||
THEAUDIODB = 'TheAudioDb',
|
||||
}
|
||||
|
||||
export enum JFImageType {
|
||||
LOGO = 'Logo',
|
||||
PRIMARY = 'Primary',
|
||||
}
|
||||
|
||||
export enum JFItemType {
|
||||
AUDIO = 'Audio',
|
||||
MUSICALBUM = 'MusicAlbum',
|
||||
}
|
||||
|
||||
export enum JFCollectionType {
|
||||
MUSIC = 'music',
|
||||
PLAYLISTS = 'playlists',
|
||||
}
|
||||
|
||||
export interface JFAuthenticate {
|
||||
AccessToken: string;
|
||||
ServerId: string;
|
||||
SessionInfo: SessionInfo;
|
||||
User: User;
|
||||
}
|
||||
|
||||
interface SessionInfo {
|
||||
AdditionalUsers: any[];
|
||||
ApplicationVersion: string;
|
||||
Capabilities: Capabilities;
|
||||
Client: string;
|
||||
DeviceId: string;
|
||||
DeviceName: string;
|
||||
HasCustomDeviceName: boolean;
|
||||
Id: string;
|
||||
IsActive: boolean;
|
||||
LastActivityDate: string;
|
||||
LastPlaybackCheckIn: string;
|
||||
NowPlayingQueue: any[];
|
||||
NowPlayingQueueFullItems: any[];
|
||||
PlayState: PlayState;
|
||||
PlayableMediaTypes: any[];
|
||||
RemoteEndPoint: string;
|
||||
ServerId: string;
|
||||
SupportedCommands: any[];
|
||||
SupportsMediaControl: boolean;
|
||||
SupportsRemoteControl: boolean;
|
||||
UserId: string;
|
||||
UserName: string;
|
||||
}
|
||||
|
||||
interface Capabilities {
|
||||
PlayableMediaTypes: any[];
|
||||
SupportedCommands: any[];
|
||||
SupportsContentUploading: boolean;
|
||||
SupportsMediaControl: boolean;
|
||||
SupportsPersistentIdentifier: boolean;
|
||||
SupportsSync: boolean;
|
||||
}
|
||||
|
||||
interface PlayState {
|
||||
CanSeek: boolean;
|
||||
IsMuted: boolean;
|
||||
IsPaused: boolean;
|
||||
RepeatMode: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
Configuration: Configuration;
|
||||
EnableAutoLogin: boolean;
|
||||
HasConfiguredEasyPassword: boolean;
|
||||
HasConfiguredPassword: boolean;
|
||||
HasPassword: boolean;
|
||||
Id: string;
|
||||
LastActivityDate: string;
|
||||
LastLoginDate: string;
|
||||
Name: string;
|
||||
Policy: Policy;
|
||||
ServerId: string;
|
||||
}
|
||||
|
||||
interface Configuration {
|
||||
DisplayCollectionsView: boolean;
|
||||
DisplayMissingEpisodes: boolean;
|
||||
EnableLocalPassword: boolean;
|
||||
EnableNextEpisodeAutoPlay: boolean;
|
||||
GroupedFolders: any[];
|
||||
HidePlayedInLatest: boolean;
|
||||
LatestItemsExcludes: any[];
|
||||
MyMediaExcludes: any[];
|
||||
OrderedViews: any[];
|
||||
PlayDefaultAudioTrack: boolean;
|
||||
RememberAudioSelections: boolean;
|
||||
RememberSubtitleSelections: boolean;
|
||||
SubtitleLanguagePreference: string;
|
||||
SubtitleMode: string;
|
||||
}
|
||||
|
||||
interface Policy {
|
||||
AccessSchedules: any[];
|
||||
AuthenticationProviderId: string;
|
||||
BlockUnratedItems: any[];
|
||||
BlockedChannels: any[];
|
||||
BlockedMediaFolders: any[];
|
||||
BlockedTags: any[];
|
||||
EnableAllChannels: boolean;
|
||||
EnableAllDevices: boolean;
|
||||
EnableAllFolders: boolean;
|
||||
EnableAudioPlaybackTranscoding: boolean;
|
||||
EnableContentDeletion: boolean;
|
||||
EnableContentDeletionFromFolders: any[];
|
||||
EnableContentDownloading: boolean;
|
||||
EnableLiveTvAccess: boolean;
|
||||
EnableLiveTvManagement: boolean;
|
||||
EnableMediaConversion: boolean;
|
||||
EnableMediaPlayback: boolean;
|
||||
EnablePlaybackRemuxing: boolean;
|
||||
EnablePublicSharing: boolean;
|
||||
EnableRemoteAccess: boolean;
|
||||
EnableRemoteControlOfOtherUsers: boolean;
|
||||
EnableSharedDeviceControl: boolean;
|
||||
EnableSyncTranscoding: boolean;
|
||||
EnableUserPreferenceAccess: boolean;
|
||||
EnableVideoPlaybackTranscoding: boolean;
|
||||
EnabledChannels: any[];
|
||||
EnabledDevices: any[];
|
||||
EnabledFolders: any[];
|
||||
ForceRemoteSourceTranscoding: boolean;
|
||||
InvalidLoginAttemptCount: number;
|
||||
IsAdministrator: boolean;
|
||||
IsDisabled: boolean;
|
||||
IsHidden: boolean;
|
||||
LoginAttemptsBeforeLockout: number;
|
||||
MaxActiveSessions: number;
|
||||
PasswordResetProviderId: string;
|
||||
RemoteClientBitrateLimit: number;
|
||||
SyncPlayAccess: string;
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
import {
|
||||
ExternalSource,
|
||||
ExternalType,
|
||||
ImageType,
|
||||
Prisma,
|
||||
Server,
|
||||
ServerFolder,
|
||||
} from '@prisma/client';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { prisma } from '@lib/prisma';
|
||||
import { uniqueArray } from '../../utils/unique-array';
|
||||
import {
|
||||
JFAlbum,
|
||||
JFAlbumArtist,
|
||||
JFExternalType,
|
||||
JFImageType,
|
||||
JFSong,
|
||||
} from './jellyfin.types';
|
||||
|
||||
const insertGenres = async (items: JFSong[] | JFAlbum[] | JFAlbumArtist[]) => {
|
||||
const genresCreateMany = items
|
||||
.flatMap((item) => item.GenreItems)
|
||||
.map((genre) => ({ name: genre.Name }));
|
||||
|
||||
await prisma.genre.createMany({
|
||||
data: genresCreateMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
const insertArtists = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder,
|
||||
items: JFSong[] | JFAlbum[]
|
||||
) => {
|
||||
const artistItems = uniqBy(
|
||||
items.flatMap((item) => item.ArtistItems),
|
||||
'Id'
|
||||
);
|
||||
|
||||
const createMany = artistItems.map((artist) => ({
|
||||
name: artist.Name,
|
||||
remoteId: artist.Id,
|
||||
serverId: server.id,
|
||||
sortName: artist.Name,
|
||||
}));
|
||||
|
||||
await prisma.artist.createMany({
|
||||
data: createMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
for (const artist of artistItems) {
|
||||
await prisma.artist.update({
|
||||
data: { serverFolders: { connect: { id: serverFolder.id } } },
|
||||
where: {
|
||||
uniqueArtistId: {
|
||||
remoteId: artist.Id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const insertImages = async (items: JFSong[] | JFAlbum[] | JFAlbumArtist[]) => {
|
||||
const imageItems = uniqBy(
|
||||
items.flatMap((item) => item.ImageTags),
|
||||
'Id'
|
||||
);
|
||||
|
||||
const createMany: Prisma.ImageCreateManyInput[] = [];
|
||||
|
||||
for (const image of imageItems) {
|
||||
if (image.Logo) {
|
||||
createMany.push({
|
||||
remoteUrl: image.Logo,
|
||||
type: ImageType.LOGO,
|
||||
});
|
||||
}
|
||||
if (image.Primary) {
|
||||
createMany.push({
|
||||
remoteUrl: image.Primary,
|
||||
type: ImageType.PRIMARY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.image.createMany({
|
||||
data: createMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
const insertExternals = async (
|
||||
items: JFSong[] | JFAlbum[] | JFAlbumArtist[]
|
||||
) => {
|
||||
const externalItems = uniqBy(
|
||||
items.flatMap((item) => item.ExternalUrls),
|
||||
'Url'
|
||||
);
|
||||
const createMany: Prisma.ExternalCreateManyInput[] = [];
|
||||
|
||||
for (const external of externalItems) {
|
||||
if (
|
||||
external.Name === JFExternalType.MUSICBRAINZ ||
|
||||
external.Name === JFExternalType.THEAUDIODB
|
||||
) {
|
||||
const source =
|
||||
external.Name === JFExternalType.MUSICBRAINZ
|
||||
? ExternalSource.MUSICBRAINZ
|
||||
: ExternalSource.THEAUDIODB;
|
||||
|
||||
const value = external.Url.split('/').pop() || '';
|
||||
|
||||
createMany.push({ source, type: ExternalType.ID, value });
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.external.createMany({
|
||||
data: createMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
const insertSongGroup = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder,
|
||||
songs: JFSong[],
|
||||
remoteAlbumId: string
|
||||
) => {
|
||||
const remoteAlbumArtist =
|
||||
songs[0].AlbumArtists.length > 0 ? songs[0].AlbumArtists[0] : undefined;
|
||||
|
||||
let albumArtist = remoteAlbumArtist?.Id
|
||||
? await prisma.albumArtist.findUnique({
|
||||
where: {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: remoteAlbumArtist.Id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// If Jellyfin returns an invalid album artist, we'll just use the first matching one
|
||||
if (remoteAlbumArtist && !albumArtist) {
|
||||
albumArtist = await prisma.albumArtist.findFirst({
|
||||
where: {
|
||||
name: remoteAlbumArtist?.Name,
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const albumArtistId = albumArtist ? albumArtist.id : undefined;
|
||||
|
||||
const songsUpsert: Prisma.SongUpsertWithWhereUniqueWithoutAlbumInput[] =
|
||||
songs.map((song) => {
|
||||
const genresConnect = song.Genres.map((genre) => ({ name: genre }));
|
||||
|
||||
const artistsConnect = song.ArtistItems.map((artist) => ({
|
||||
uniqueArtistId: {
|
||||
remoteId: artist.Id,
|
||||
serverId: server.id,
|
||||
},
|
||||
}));
|
||||
|
||||
const externalsConnect = song.ExternalUrls.map((external) => ({
|
||||
uniqueExternalId: {
|
||||
source:
|
||||
external.Name === JFExternalType.MUSICBRAINZ
|
||||
? ExternalSource.MUSICBRAINZ
|
||||
: ExternalSource.THEAUDIODB,
|
||||
value: external.Url.split('/').pop() || '',
|
||||
},
|
||||
}));
|
||||
|
||||
const imagesConnectOrCreate = [];
|
||||
for (const [key, value] of Object.entries(song.ImageTags)) {
|
||||
if (key === JFImageType.PRIMARY) {
|
||||
imagesConnectOrCreate.push({
|
||||
create: {
|
||||
remoteUrl: value,
|
||||
type: ImageType.PRIMARY,
|
||||
},
|
||||
where: {
|
||||
uniqueImageId: { remoteUrl: value, type: ImageType.PRIMARY },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (key === JFImageType.LOGO) {
|
||||
imagesConnectOrCreate.push({
|
||||
create: {
|
||||
remoteUrl: value,
|
||||
type: ImageType.LOGO,
|
||||
},
|
||||
where: {
|
||||
uniqueImageId: { remoteUrl: value, type: ImageType.LOGO },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const pathSplit = song.MediaSources[0].Path.split('/');
|
||||
const parentPath = pathSplit.slice(0, pathSplit.length - 1).join('/');
|
||||
|
||||
return {
|
||||
create: {
|
||||
albumArtistId,
|
||||
artists: { connect: artistsConnect },
|
||||
bitRate: Math.floor(song.MediaSources[0].Bitrate / 1e3),
|
||||
container: song.MediaSources[0].Container,
|
||||
deleted: false,
|
||||
discNumber: song.ParentIndexNumber,
|
||||
duration: Math.floor(song.MediaSources[0].RunTimeTicks / 1e7),
|
||||
externals: { connect: externalsConnect },
|
||||
folders: {
|
||||
connect: {
|
||||
uniqueFolderId: { path: parentPath, serverId: server.id },
|
||||
},
|
||||
},
|
||||
genres: { connect: genresConnect },
|
||||
images: { connectOrCreate: imagesConnectOrCreate },
|
||||
name: song.Name,
|
||||
releaseDate: song.PremiereDate,
|
||||
releaseYear: song.ProductionYear,
|
||||
remoteCreatedAt: song.DateCreated,
|
||||
remoteId: song.Id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
size: song.MediaSources[0].Size,
|
||||
sortName: song.Name,
|
||||
trackNumber: song.IndexNumber,
|
||||
},
|
||||
update: {
|
||||
albumArtistId,
|
||||
artists: { connect: artistsConnect },
|
||||
bitRate: Math.floor(song.MediaSources[0].Bitrate / 1e3),
|
||||
container: song.MediaSources[0].Container,
|
||||
deleted: false,
|
||||
discNumber: song.ParentIndexNumber,
|
||||
duration: Math.floor(song.MediaSources[0].RunTimeTicks / 1e7),
|
||||
externals: { connect: externalsConnect },
|
||||
folders: {
|
||||
connect: {
|
||||
uniqueFolderId: { path: parentPath, serverId: server.id },
|
||||
},
|
||||
},
|
||||
genres: { connect: genresConnect },
|
||||
images: { connectOrCreate: imagesConnectOrCreate },
|
||||
name: song.Name,
|
||||
releaseDate: song.PremiereDate,
|
||||
releaseYear: song.ProductionYear,
|
||||
remoteCreatedAt: song.DateCreated,
|
||||
remoteId: song.Id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
size: song.MediaSources[0].Size,
|
||||
sortName: song.Name,
|
||||
trackNumber: song.IndexNumber,
|
||||
},
|
||||
where: {
|
||||
uniqueSongId: {
|
||||
remoteId: song.Id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const uniqueArtistIds = songs
|
||||
.flatMap((song) => song.ArtistItems.flatMap((artist) => artist.Id))
|
||||
.filter(uniqueArray);
|
||||
|
||||
const artistsConnect = uniqueArtistIds.map((artistId) => ({
|
||||
uniqueArtistId: {
|
||||
remoteId: artistId,
|
||||
serverId: server.id,
|
||||
},
|
||||
}));
|
||||
|
||||
await prisma.album.update({
|
||||
data: {
|
||||
artists: { connect: artistsConnect },
|
||||
deleted: false,
|
||||
songs: { upsert: songsUpsert },
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumId: {
|
||||
remoteId: remoteAlbumId,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const jellyfinUtils = {
|
||||
insertArtists,
|
||||
insertExternals,
|
||||
insertGenres,
|
||||
insertImages,
|
||||
insertSongGroup,
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { navidromeApi } from './navidrome.api';
|
||||
import { navidromeScanner } from './navidrome.scanner';
|
||||
|
||||
export const navidrome = {
|
||||
api: navidromeApi,
|
||||
scanner: navidromeScanner,
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Server } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
NDAlbumListResponse,
|
||||
NDGenreListResponse,
|
||||
NDAlbumListParams,
|
||||
NDGenreListParams,
|
||||
NDSongListParams,
|
||||
NDSongListResponse,
|
||||
NDArtistListResponse,
|
||||
NDAuthenticate,
|
||||
} from './navidrome.types';
|
||||
|
||||
const api = axios.create();
|
||||
|
||||
const authenticate = async (options: {
|
||||
password: string;
|
||||
url: string;
|
||||
username: string;
|
||||
}) => {
|
||||
const { password, url, username } = options;
|
||||
const cleanServerUrl = url.replace(/\/$/, '');
|
||||
|
||||
const { data } = await api.post<NDAuthenticate>(
|
||||
`${cleanServerUrl}/auth/login`,
|
||||
{ password, username }
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getGenres = async (server: Server, params?: NDGenreListParams) => {
|
||||
const { data } = await api.get<NDGenreListResponse>(
|
||||
`${server.url}/api/genre`,
|
||||
{
|
||||
headers: { 'x-nd-authorization': `Bearer ${server.token}` },
|
||||
params,
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getArtists = async (server: Server, params?: NDGenreListParams) => {
|
||||
const { data } = await api.get<NDArtistListResponse>(
|
||||
`${server.url}/api/artist`,
|
||||
{
|
||||
headers: { 'x-nd-authorization': `Bearer ${server.token}` },
|
||||
params,
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getAlbums = async (server: Server, params?: NDAlbumListParams) => {
|
||||
const { data } = await api.get<NDAlbumListResponse>(
|
||||
`${server.url}/api/album`,
|
||||
{
|
||||
headers: { 'x-nd-authorization': `Bearer ${server.token}` },
|
||||
params,
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getSongs = async (server: Server, params?: NDSongListParams) => {
|
||||
const { data } = await api.get<NDSongListResponse>(`${server.url}/api/song`, {
|
||||
headers: { 'x-nd-authorization': `Bearer ${server.token}` },
|
||||
params,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const navidromeApi = {
|
||||
authenticate,
|
||||
getAlbums,
|
||||
getArtists,
|
||||
getGenres,
|
||||
getSongs,
|
||||
};
|
||||
@@ -1,376 +0,0 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import {
|
||||
ExternalSource,
|
||||
ExternalType,
|
||||
Folder,
|
||||
ImageType,
|
||||
Server,
|
||||
ServerFolder,
|
||||
Task,
|
||||
} from '@prisma/client';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { prisma } from '@lib/prisma';
|
||||
import { groupByProperty } from '@utils/group-by-property';
|
||||
import { queue } from '../queues/index';
|
||||
import { navidromeApi } from './navidrome.api';
|
||||
import { navidromeUtils } from './navidrome.utils';
|
||||
|
||||
const CHUNK_SIZE = 5000;
|
||||
|
||||
export const scanGenres = async (server: Server, task: Task) => {
|
||||
await prisma.task.update({
|
||||
data: { message: 'Scanning genres' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
const res = await navidromeApi.getGenres(server);
|
||||
|
||||
const genres = res.map((genre) => {
|
||||
return { name: genre.name };
|
||||
});
|
||||
|
||||
await prisma.genre.createMany({
|
||||
data: genres,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const scanAlbumArtists = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder
|
||||
) => {
|
||||
const artists = await navidromeApi.getArtists(server);
|
||||
|
||||
const externalsCreateMany = artists
|
||||
.filter((artist) => artist.mbzArtistId)
|
||||
.map((artist) => ({
|
||||
source: ExternalSource.MUSICBRAINZ,
|
||||
type: ExternalType.ID,
|
||||
value: artist.mbzArtistId,
|
||||
}));
|
||||
|
||||
await prisma.external.createMany({
|
||||
data: externalsCreateMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
for (const artist of artists) {
|
||||
const genresConnect = artist.genres
|
||||
? artist.genres.map((genre) => ({ name: genre.name }))
|
||||
: undefined;
|
||||
|
||||
const externalsConnect = artist.mbzArtistId
|
||||
? {
|
||||
uniqueExternalId: {
|
||||
source: ExternalSource.MUSICBRAINZ,
|
||||
value: artist.mbzArtistId,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await prisma.albumArtist.upsert({
|
||||
create: {
|
||||
deleted: false,
|
||||
externals: { connect: externalsConnect },
|
||||
genres: { connect: genresConnect },
|
||||
name: artist.name,
|
||||
remoteId: artist.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: artist.name,
|
||||
},
|
||||
update: {
|
||||
deleted: false,
|
||||
externals: { connect: externalsConnect },
|
||||
genres: { connect: genresConnect },
|
||||
name: artist.name,
|
||||
remoteId: artist.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: artist.name,
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: artist.id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const scanAlbums = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder
|
||||
) => {
|
||||
let start = 0;
|
||||
let count = 5000;
|
||||
do {
|
||||
const albums = await navidromeApi.getAlbums(server, {
|
||||
_end: start + CHUNK_SIZE,
|
||||
_start: start,
|
||||
});
|
||||
|
||||
const imagesCreateMany = albums
|
||||
.filter((album) => album.coverArtId)
|
||||
.map((album) => ({
|
||||
remoteUrl: album.coverArtId,
|
||||
type: ImageType.PRIMARY,
|
||||
}));
|
||||
|
||||
await prisma.image.createMany({
|
||||
data: imagesCreateMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
const artistIds = (
|
||||
await prisma.artist.findMany({
|
||||
select: { remoteId: true },
|
||||
where: { serverId: server.id },
|
||||
})
|
||||
).map((artist) => artist.remoteId);
|
||||
|
||||
for (const album of albums) {
|
||||
const imagesConnect = album.coverArtId
|
||||
? {
|
||||
uniqueImageId: {
|
||||
remoteUrl: album.coverArtId,
|
||||
type: ImageType.PRIMARY,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const genresConnect = album.genres
|
||||
? album.genres.map((genre) => ({ name: genre.name }))
|
||||
: undefined;
|
||||
|
||||
const validArtistIds = [];
|
||||
const ndArtistIds = album.allArtistIds.split(' ');
|
||||
|
||||
for (const artistId of ndArtistIds) {
|
||||
if (artistIds.includes(artistId)) {
|
||||
validArtistIds.push(artistId);
|
||||
}
|
||||
}
|
||||
|
||||
const artistsConnect = validArtistIds.map((id) => ({
|
||||
uniqueArtistId: {
|
||||
remoteId: id,
|
||||
serverId: server.id,
|
||||
},
|
||||
}));
|
||||
|
||||
const albumArtistConnect = album.artistId
|
||||
? {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: album.artistId,
|
||||
serverId: server.id,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await prisma.album.upsert({
|
||||
create: {
|
||||
albumArtists: { connect: albumArtistConnect },
|
||||
artists: { connect: artistsConnect },
|
||||
deleted: false,
|
||||
genres: { connect: genresConnect },
|
||||
images: { connect: imagesConnect },
|
||||
name: album.name,
|
||||
releaseDate: album?.minYear
|
||||
? new Date(album.minYear, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: album.minYear,
|
||||
remoteCreatedAt: album.createdAt,
|
||||
remoteId: album.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: album.name,
|
||||
},
|
||||
update: {
|
||||
albumArtists: { connect: albumArtistConnect },
|
||||
artists: { connect: artistsConnect },
|
||||
deleted: false,
|
||||
genres: { connect: genresConnect },
|
||||
images: { connect: imagesConnect },
|
||||
name: album.name,
|
||||
releaseDate: album?.minYear
|
||||
? new Date(album.minYear, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: album.minYear,
|
||||
remoteCreatedAt: album.createdAt,
|
||||
remoteId: album.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: album.name,
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumId: {
|
||||
remoteId: album.id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
start += CHUNK_SIZE;
|
||||
count = albums.length;
|
||||
} while (count === CHUNK_SIZE);
|
||||
};
|
||||
|
||||
const scanSongs = async (server: Server, serverFolder: ServerFolder) => {
|
||||
let start = 0;
|
||||
let count = 5000;
|
||||
do {
|
||||
const songs = await navidromeApi.getSongs(server, {
|
||||
_end: start + CHUNK_SIZE,
|
||||
_start: start,
|
||||
});
|
||||
|
||||
const externalsCreateMany = [];
|
||||
const genresCreateMany = [];
|
||||
for (const song of songs) {
|
||||
if (song.mbzTrackId) {
|
||||
externalsCreateMany.push({
|
||||
source: ExternalSource.MUSICBRAINZ,
|
||||
type: ExternalType.ID,
|
||||
value: song.mbzTrackId,
|
||||
});
|
||||
}
|
||||
|
||||
if (song.genres?.length > 0) {
|
||||
genresCreateMany.push(
|
||||
...song.genres.map((genre) => ({ name: genre.name }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.external.createMany({
|
||||
data: externalsCreateMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
await prisma.genre.createMany({
|
||||
data: genresCreateMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
const folderGroups = songs.map((song) => {
|
||||
const songPaths = song.path.split('/');
|
||||
const paths = [];
|
||||
for (let b = 0; b < songPaths.length - 1; b += 1) {
|
||||
paths.push({
|
||||
name: songPaths[b],
|
||||
path: songPaths.slice(0, b + 1).join('/'),
|
||||
});
|
||||
}
|
||||
|
||||
return paths;
|
||||
});
|
||||
|
||||
const uniqueFolders = uniqBy(
|
||||
folderGroups.flatMap((folder) => folder).filter((f) => f.path !== ''),
|
||||
'path'
|
||||
);
|
||||
|
||||
const createdFolders: Folder[] = [];
|
||||
for (const folder of uniqueFolders) {
|
||||
const createdFolder = await prisma.folder.upsert({
|
||||
create: {
|
||||
name: folder.name,
|
||||
path: folder.path,
|
||||
serverFolders: {
|
||||
connect: {
|
||||
uniqueServerFolderId: {
|
||||
remoteId: serverFolder.remoteId,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
serverId: server.id,
|
||||
},
|
||||
update: {
|
||||
name: folder.name,
|
||||
path: folder.path,
|
||||
serverFolders: {
|
||||
connect: {
|
||||
uniqueServerFolderId: {
|
||||
remoteId: serverFolder.remoteId,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
uniqueFolderId: {
|
||||
path: folder.path,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createdFolders.push(createdFolder);
|
||||
}
|
||||
|
||||
for (const folder of createdFolders) {
|
||||
if (folder.parentId) break;
|
||||
|
||||
const pathSplit = folder.path.split('/');
|
||||
const parentPath = pathSplit.slice(0, pathSplit.length - 1).join('/');
|
||||
|
||||
const parentPathData = createdFolders.find(
|
||||
(save) => save.path === parentPath
|
||||
);
|
||||
|
||||
if (parentPathData) {
|
||||
await prisma.folder.update({
|
||||
data: {
|
||||
parentId: parentPathData.id,
|
||||
},
|
||||
where: { id: folder.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const albumSongGroups = groupByProperty(songs, 'albumId');
|
||||
const albumIds = Object.keys(albumSongGroups);
|
||||
|
||||
for (const id of albumIds) {
|
||||
const songGroup = albumSongGroups[id];
|
||||
await navidromeUtils.insertSongGroup(server, serverFolder, songGroup, id);
|
||||
}
|
||||
|
||||
start += CHUNK_SIZE;
|
||||
count = songs.length;
|
||||
} while (count === CHUNK_SIZE);
|
||||
};
|
||||
|
||||
const scanAll = async (
|
||||
server: Server,
|
||||
serverFolders: ServerFolder[],
|
||||
task: Task
|
||||
) => {
|
||||
queue.scanner.push({
|
||||
fn: async () => {
|
||||
await prisma.task.update({
|
||||
data: { message: 'Beginning scan...' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
for (const serverFolder of serverFolders) {
|
||||
await scanGenres(server, task);
|
||||
await scanAlbumArtists(server, serverFolder);
|
||||
await scanAlbums(server, serverFolder);
|
||||
await scanSongs(server, serverFolder);
|
||||
}
|
||||
|
||||
return { task };
|
||||
},
|
||||
id: task.id,
|
||||
});
|
||||
};
|
||||
|
||||
export const navidromeScanner = {
|
||||
scanAll,
|
||||
scanGenres,
|
||||
};
|
||||
@@ -1,169 +0,0 @@
|
||||
export type NDAuthenticate = {
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
name: string;
|
||||
subsonicSalt: string;
|
||||
subsonicToken: string;
|
||||
token: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type NDGenre = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type NDAlbum = {
|
||||
albumArtist: string;
|
||||
albumArtistId: string;
|
||||
allArtistIds: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
compilation: boolean;
|
||||
coverArtId: string;
|
||||
coverArtPath: string;
|
||||
createdAt: string;
|
||||
duration: number;
|
||||
fullText: string;
|
||||
genre: string;
|
||||
genres: NDGenre[];
|
||||
id: string;
|
||||
maxYear: number;
|
||||
mbzAlbumArtistId: string;
|
||||
mbzAlbumId: string;
|
||||
minYear: number;
|
||||
name: string;
|
||||
orderAlbumArtistName: string;
|
||||
orderAlbumName: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
songCount: number;
|
||||
sortAlbumArtistName: string;
|
||||
sortArtistName: string;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type NDSong = {
|
||||
album: string;
|
||||
albumArtist: string;
|
||||
albumArtistId: string;
|
||||
albumId: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
bitRate: number;
|
||||
bookmarkPosition: number;
|
||||
channels: number;
|
||||
compilation: boolean;
|
||||
createdAt: string;
|
||||
discNumber: number;
|
||||
duration: number;
|
||||
fullText: string;
|
||||
genre: string;
|
||||
genres: NDGenre[];
|
||||
hasCoverArt: boolean;
|
||||
id: string;
|
||||
mbzAlbumArtistId: string;
|
||||
mbzAlbumId: string;
|
||||
mbzArtistId: string;
|
||||
mbzTrackId: string;
|
||||
orderAlbumArtistName: string;
|
||||
orderAlbumName: string;
|
||||
orderArtistName: string;
|
||||
orderTitle: string;
|
||||
path: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
sortAlbumArtistName: string;
|
||||
sortArtistName: string;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
suffix: string;
|
||||
title: string;
|
||||
trackNumber: number;
|
||||
updatedAt: string;
|
||||
year: number;
|
||||
};
|
||||
|
||||
export type NDArtist = {
|
||||
albumCount: number;
|
||||
biography: string;
|
||||
externalInfoUpdatedAt: string;
|
||||
externalUrl: string;
|
||||
fullText: string;
|
||||
genres: NDGenre[];
|
||||
id: string;
|
||||
largeImageUrl: string;
|
||||
mbzArtistId: string;
|
||||
mediumImageUrl: string;
|
||||
name: string;
|
||||
orderArtistName: string;
|
||||
playCount: number;
|
||||
playDate: string;
|
||||
rating: number;
|
||||
size: number;
|
||||
smallImageUrl: string;
|
||||
songCount: number;
|
||||
starred: boolean;
|
||||
starredAt: string;
|
||||
};
|
||||
|
||||
export type NDGenreListResponse = NDGenre[];
|
||||
|
||||
export type NDAlbumListResponse = NDAlbum[];
|
||||
|
||||
export type NDSongListResponse = NDSong[];
|
||||
|
||||
export type NDArtistListResponse = NDArtist[];
|
||||
|
||||
export type NDPagination = {
|
||||
_end?: number;
|
||||
_start?: number;
|
||||
};
|
||||
|
||||
export type NDOrder = {
|
||||
_order?: 'ASC' | 'DESC';
|
||||
};
|
||||
|
||||
export enum NDGenreSort {
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export type NDGenreListParams = {
|
||||
_sort?: NDGenreSort;
|
||||
id?: string;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
|
||||
export enum NDAlbumSort {
|
||||
ARTIST = 'artist',
|
||||
MAX_YEAR = 'max_year',
|
||||
NAME = 'name',
|
||||
RANDOM = 'random',
|
||||
RECENTLY_ADDED = 'recently_added',
|
||||
}
|
||||
|
||||
export type NDAlbumListParams = {
|
||||
_sort?: NDAlbumSort;
|
||||
artist_id?: string;
|
||||
compilation?: boolean;
|
||||
genre_id?: string;
|
||||
has_rating?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
recently_played?: boolean;
|
||||
starred?: boolean;
|
||||
year?: number;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
|
||||
export type NDSongListParams = {
|
||||
genre_id?: string;
|
||||
starred?: boolean;
|
||||
} & NDPagination &
|
||||
NDOrder;
|
||||
@@ -1,125 +0,0 @@
|
||||
import { ExternalSource, Server, ServerFolder } from '@prisma/client';
|
||||
import { prisma } from '@lib/prisma';
|
||||
import { NDSong } from './navidrome.types';
|
||||
|
||||
const insertSongGroup = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder,
|
||||
songs: NDSong[],
|
||||
remoteAlbumId: string
|
||||
) => {
|
||||
const songsWithArtistIds = songs.filter((song) => song.artistId);
|
||||
const artistId =
|
||||
songsWithArtistIds.length > 0 ? songsWithArtistIds[0].artistId : undefined;
|
||||
|
||||
const albumArtist = artistId
|
||||
? await prisma.albumArtist.findUnique({
|
||||
where: {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: artistId,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const songsUpsert = songs.map((song) => {
|
||||
const genresConnect = song.genres
|
||||
? song.genres.map((genre) => ({ name: genre.name }))
|
||||
: undefined;
|
||||
|
||||
const externalsConnect = song.mbzTrackId
|
||||
? {
|
||||
uniqueExternalId: {
|
||||
source: ExternalSource.MUSICBRAINZ,
|
||||
value: song.mbzTrackId,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const pathSplit = song.path.split('/');
|
||||
const parentPath = pathSplit.slice(0, pathSplit.length - 1).join('/');
|
||||
|
||||
return {
|
||||
create: {
|
||||
albumArtistId: albumArtist?.id,
|
||||
artistName: !song.artistId ? song.artist : undefined,
|
||||
bitRate: song.bitRate,
|
||||
container: song.suffix,
|
||||
deleted: false,
|
||||
discNumber: song.discNumber,
|
||||
duration: song.duration,
|
||||
externals: { connect: externalsConnect },
|
||||
folders: {
|
||||
connect: {
|
||||
uniqueFolderId: { path: parentPath, serverId: server.id },
|
||||
},
|
||||
},
|
||||
genres: { connect: genresConnect },
|
||||
name: song.title,
|
||||
releaseDate: song?.year
|
||||
? new Date(song.year, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: song?.year,
|
||||
remoteCreatedAt: song.createdAt,
|
||||
remoteId: song.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
size: song.size,
|
||||
sortName: song.title,
|
||||
trackNumber: song.trackNumber,
|
||||
},
|
||||
update: {
|
||||
albumArtistId: albumArtist?.id,
|
||||
artistName: !song.artistId ? song.artist : undefined,
|
||||
bitRate: song.bitRate,
|
||||
container: song.suffix,
|
||||
deleted: false,
|
||||
discNumber: song.discNumber,
|
||||
duration: song.duration,
|
||||
externals: { connect: externalsConnect },
|
||||
folders: {
|
||||
connect: {
|
||||
uniqueFolderId: { path: parentPath, serverId: server.id },
|
||||
},
|
||||
},
|
||||
genres: { connect: genresConnect },
|
||||
name: song.title,
|
||||
releaseDate: song?.year
|
||||
? new Date(song.year, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: song?.year,
|
||||
remoteCreatedAt: song.createdAt,
|
||||
remoteId: song.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
size: song.size,
|
||||
sortName: song.title,
|
||||
trackNumber: song.trackNumber,
|
||||
},
|
||||
where: {
|
||||
uniqueSongId: {
|
||||
remoteId: song.id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await prisma.album.update({
|
||||
data: {
|
||||
deleted: false,
|
||||
songs: { upsert: songsUpsert },
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumId: {
|
||||
remoteId: remoteAlbumId,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const navidromeUtils = {
|
||||
insertSongGroup,
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { scannerQueue } from './scanner.queue';
|
||||
|
||||
export const queue = {
|
||||
scanner: scannerQueue,
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Task } from '@prisma/client';
|
||||
import Queue from 'better-queue';
|
||||
import { prisma } from '../../lib';
|
||||
|
||||
interface QueueTask {
|
||||
fn: any;
|
||||
id: string;
|
||||
task: Task;
|
||||
}
|
||||
|
||||
export const scannerQueue: Queue = new Queue(
|
||||
async (task: QueueTask, cb: any) => {
|
||||
const result = await task.fn();
|
||||
return cb(null, result);
|
||||
},
|
||||
{
|
||||
afterProcessDelay: 1000,
|
||||
cancelIfRunning: true,
|
||||
concurrent: 1,
|
||||
filo: false,
|
||||
maxRetries: 5,
|
||||
maxTimeout: 600000,
|
||||
retryDelay: 2000,
|
||||
}
|
||||
);
|
||||
|
||||
scannerQueue.on('task_finish', async (taskId) => {
|
||||
await prisma.task.update({
|
||||
data: {
|
||||
completed: true,
|
||||
isError: false,
|
||||
progress: null,
|
||||
},
|
||||
where: { id: taskId },
|
||||
});
|
||||
});
|
||||
|
||||
scannerQueue.on('task_failed', async (taskId, errorMessage) => {
|
||||
const dbTaskId = taskId.split('(')[1].split(')')[0];
|
||||
|
||||
console.log('errorMessage', errorMessage);
|
||||
await prisma.task.update({
|
||||
data: {
|
||||
completed: true,
|
||||
isError: true,
|
||||
message: errorMessage,
|
||||
},
|
||||
where: { id: dbTaskId },
|
||||
});
|
||||
});
|
||||
|
||||
scannerQueue.on('drain', async () => {
|
||||
await prisma.task.updateMany({
|
||||
data: { completed: true, progress: null },
|
||||
where: { completed: false },
|
||||
});
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import { subsonicApi } from './subsonic.api';
|
||||
import { subsonicScanner } from './subsonic.scanner';
|
||||
|
||||
export const subsonic = {
|
||||
api: subsonicApi,
|
||||
scanner: subsonicScanner,
|
||||
};
|
||||
@@ -1,157 +0,0 @@
|
||||
import { Server } from '@prisma/client';
|
||||
import axios from 'axios';
|
||||
import md5 from 'md5';
|
||||
import { randomString } from '@utils/index';
|
||||
import {
|
||||
SSAlbumListEntry,
|
||||
SSAlbumListResponse,
|
||||
SSAlbumResponse,
|
||||
SSAlbumsParams,
|
||||
SSArtistIndex,
|
||||
SSArtistInfoResponse,
|
||||
SSArtistsResponse,
|
||||
SSGenresResponse,
|
||||
SSMusicFoldersResponse,
|
||||
} from './subsonic.types';
|
||||
|
||||
const api = axios.create({
|
||||
validateStatus: (status) => status >= 200,
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(res: any) => {
|
||||
res.data = res.data['subsonic-response'];
|
||||
return res;
|
||||
},
|
||||
(err: any) => {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
const authenticate = async (options: {
|
||||
legacy?: boolean;
|
||||
password: string;
|
||||
url: string;
|
||||
username: string;
|
||||
}) => {
|
||||
let token;
|
||||
|
||||
const cleanServerUrl = options.url.replace(/\/$/, '');
|
||||
|
||||
if (options.legacy) {
|
||||
token = `u=${options.username}&p=${options.password}`;
|
||||
} else {
|
||||
const salt = randomString(12);
|
||||
const hash = md5(options.password + salt);
|
||||
token = `u=${options.username}&s=${salt}&t=${hash}`;
|
||||
}
|
||||
|
||||
const { data } = await api.get(
|
||||
`${cleanServerUrl}/rest/ping.view?v=1.13.0&c=sonixd&f=json&${token}`
|
||||
);
|
||||
|
||||
return { token, ...data };
|
||||
};
|
||||
|
||||
const getMusicFolders = async (server: Partial<Server>) => {
|
||||
const { data } = await api.get<SSMusicFoldersResponse>(
|
||||
`${server.url}/rest/getMusicFolders.view?v=1.13.0&c=sonixd&f=json&${server.token}`
|
||||
);
|
||||
|
||||
return data.musicFolders.musicFolder;
|
||||
};
|
||||
|
||||
const getArtists = async (server: Server, musicFolderId: string) => {
|
||||
const { data } = await api.get<SSArtistsResponse>(
|
||||
`${server.url}/rest/getArtists.view?v=1.13.0&c=sonixd&f=json&${server.token}`,
|
||||
{ params: { musicFolderId } }
|
||||
);
|
||||
|
||||
const artists = (data.artists?.index || []).flatMap(
|
||||
(index: SSArtistIndex) => index.artist
|
||||
);
|
||||
|
||||
return artists;
|
||||
};
|
||||
|
||||
const getGenres = async (server: Server) => {
|
||||
const { data: genres } = await api.get<SSGenresResponse>(
|
||||
`${server.url}/rest/getGenres.view?v=1.13.0&c=sonixd&f=json&${server.token}`
|
||||
);
|
||||
|
||||
return genres;
|
||||
};
|
||||
|
||||
const getAlbum = async (server: Server, id: string) => {
|
||||
const { data: album } = await api.get<SSAlbumResponse>(
|
||||
`${server.url}/rest/getAlbum.view?v=1.13.0&c=sonixd&f=json&${server.token}`,
|
||||
{ params: { id } }
|
||||
);
|
||||
|
||||
return album;
|
||||
};
|
||||
|
||||
const getAlbums = async (
|
||||
server: Server,
|
||||
params: SSAlbumsParams,
|
||||
recursiveData: any[] = []
|
||||
) => {
|
||||
const albums: any = api
|
||||
.get<SSAlbumListResponse>(
|
||||
`${server.url}/rest/getAlbumList2.view?v=1.13.0&c=sonixd&f=json&${server.token}`,
|
||||
{ params }
|
||||
)
|
||||
.then((res) => {
|
||||
if (
|
||||
!res.data.albumList2.album ||
|
||||
res.data.albumList2.album.length === 0
|
||||
) {
|
||||
// Flatten and return once there are no more albums left
|
||||
return recursiveData.flatMap((album) => album);
|
||||
}
|
||||
|
||||
// On every iteration, push the existing combined album array and increase the offset
|
||||
recursiveData.push(res.data.albumList2.album);
|
||||
return getAlbums(
|
||||
server,
|
||||
{
|
||||
musicFolderId: params.musicFolderId,
|
||||
offset: (params.offset || 0) + (params.size || 0),
|
||||
size: params.size,
|
||||
type: 'newest',
|
||||
},
|
||||
|
||||
recursiveData
|
||||
);
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
return albums as SSAlbumListEntry[];
|
||||
};
|
||||
|
||||
const getArtistInfo = async (server: Server, id: string) => {
|
||||
const { data: artistInfo } = await api.get<SSArtistInfoResponse>(
|
||||
`${server.url}/rest/getArtistInfo2.view?v=1.13.0&c=sonixd&f=json&${server.token}`,
|
||||
{ params: { id } }
|
||||
);
|
||||
|
||||
return {
|
||||
...artistInfo,
|
||||
artistInfo2: {
|
||||
...artistInfo.artistInfo2,
|
||||
biography: artistInfo.artistInfo2.biography
|
||||
.replaceAll(/<a target.*<\/a>/gm, '')
|
||||
.replace('Biography not available', ''),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const subsonicApi = {
|
||||
authenticate,
|
||||
getAlbum,
|
||||
getAlbums,
|
||||
getArtistInfo,
|
||||
getArtists,
|
||||
getGenres,
|
||||
getMusicFolders,
|
||||
};
|
||||
@@ -1,288 +0,0 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ImageType, Server, ServerFolder, Task } from '@prisma/client';
|
||||
import { prisma, throttle } from '@lib/index';
|
||||
import { uniqueArray } from '@utils/index';
|
||||
import { queue } from '../queues';
|
||||
import { subsonicApi } from './subsonic.api';
|
||||
import { subsonicUtils } from './subsonic.utils';
|
||||
|
||||
export const scanGenres = async (server: Server, task: Task) => {
|
||||
await prisma.task.update({
|
||||
data: { message: 'Scanning genres' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
const res = await subsonicApi.getGenres(server);
|
||||
|
||||
const genres = res.genres.genre.map((genre) => {
|
||||
return { name: genre.value };
|
||||
});
|
||||
|
||||
await prisma.genre.createMany({
|
||||
data: genres,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const scanAlbumArtists = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder
|
||||
) => {
|
||||
const artists = await subsonicApi.getArtists(server, serverFolder.remoteId);
|
||||
|
||||
for (const artist of artists) {
|
||||
await prisma.albumArtist.upsert({
|
||||
create: {
|
||||
name: artist.name,
|
||||
remoteId: artist.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: artist.name,
|
||||
},
|
||||
update: {
|
||||
name: artist.name,
|
||||
remoteId: artist.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: artist.name,
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: artist.id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const scanAlbums = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder
|
||||
) => {
|
||||
const albums = await subsonicApi.getAlbums(server, {
|
||||
musicFolderId: serverFolder.id,
|
||||
offset: 0,
|
||||
size: 500,
|
||||
type: 'newest',
|
||||
});
|
||||
|
||||
await subsonicUtils.insertImages(albums);
|
||||
|
||||
for (const album of albums) {
|
||||
const imagesConnect = album.coverArt
|
||||
? {
|
||||
uniqueImageId: {
|
||||
remoteUrl: album.coverArt,
|
||||
type: ImageType.PRIMARY,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const albumArtistConnect = album.artistId
|
||||
? {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: album.artistId,
|
||||
serverId: server.id,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await prisma.album.upsert({
|
||||
create: {
|
||||
albumArtists: { connect: albumArtistConnect },
|
||||
genres: { connect: album.genre ? { name: album.genre } : undefined },
|
||||
images: { connect: imagesConnect },
|
||||
name: album.title,
|
||||
releaseDate: album?.year
|
||||
? new Date(album.year, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: album.year,
|
||||
remoteCreatedAt: album.created,
|
||||
remoteId: album.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: album.title,
|
||||
},
|
||||
update: {
|
||||
albumArtists: { connect: albumArtistConnect },
|
||||
genres: { connect: album.genre ? { name: album.genre } : undefined },
|
||||
images: { connect: imagesConnect },
|
||||
name: album.title,
|
||||
releaseDate: album?.year
|
||||
? new Date(album.year, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: album.year,
|
||||
remoteCreatedAt: album.created,
|
||||
remoteId: album.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
sortName: album.title,
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumId: {
|
||||
remoteId: album.id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const throttledAlbumFetch = throttle(
|
||||
async (server: Server, serverFolder: ServerFolder, album: any) => {
|
||||
const albumRes = await subsonicApi.getAlbum(server, album.remoteId);
|
||||
|
||||
if (albumRes) {
|
||||
await subsonicUtils.insertSongImages(albumRes);
|
||||
const songsUpsert = albumRes.album.song.map((song) => {
|
||||
const genresConnect = song.genre ? { name: song.genre } : undefined;
|
||||
|
||||
const imagesConnect = song.coverArt
|
||||
? {
|
||||
uniqueImageId: {
|
||||
remoteUrl: song.coverArt,
|
||||
type: ImageType.PRIMARY,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const albumArtistsConnect = song.artistId
|
||||
? {
|
||||
uniqueAlbumArtistId: {
|
||||
remoteId: song.artistId,
|
||||
serverId: server.id,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
create: {
|
||||
albumArtists: { connect: albumArtistsConnect },
|
||||
artistName: !song.artistId ? song.artist : undefined,
|
||||
bitRate: song.bitRate,
|
||||
container: song.suffix,
|
||||
discNumber: song.discNumber,
|
||||
duration: song.duration,
|
||||
genres: { connect: genresConnect },
|
||||
images: { connect: imagesConnect },
|
||||
name: song.title,
|
||||
releaseDate: song?.year
|
||||
? new Date(song.year, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: song.year,
|
||||
remoteCreatedAt: song.created,
|
||||
remoteId: song.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
size: song.size,
|
||||
sortName: song.title,
|
||||
trackNumber: song.track,
|
||||
},
|
||||
update: {
|
||||
albumArtists: { connect: albumArtistsConnect },
|
||||
artistName: !song.artistId ? song.artist : undefined,
|
||||
bitRate: song.bitRate,
|
||||
container: song.suffix,
|
||||
discNumber: song.discNumber,
|
||||
duration: song.duration,
|
||||
genres: { connect: genresConnect },
|
||||
images: { connect: imagesConnect },
|
||||
name: song.title,
|
||||
releaseDate: song?.year
|
||||
? new Date(song.year, 0).toISOString()
|
||||
: undefined,
|
||||
releaseYear: song.year,
|
||||
remoteCreatedAt: song.created,
|
||||
remoteId: song.id,
|
||||
serverFolders: { connect: { id: serverFolder.id } },
|
||||
serverId: server.id,
|
||||
size: song.size,
|
||||
sortName: song.title,
|
||||
trackNumber: song.track,
|
||||
},
|
||||
where: {
|
||||
uniqueSongId: {
|
||||
remoteId: song.id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const uniqueArtistIds = albumRes.album.song
|
||||
.map((song) => song.artistId)
|
||||
.filter(uniqueArray);
|
||||
|
||||
const artistsConnect = uniqueArtistIds.map((artistId) => {
|
||||
return {
|
||||
uniqueArtistId: {
|
||||
remoteId: artistId!,
|
||||
serverId: server.id,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await prisma.album.update({
|
||||
data: {
|
||||
artists: { connect: artistsConnect },
|
||||
songs: { upsert: songsUpsert },
|
||||
},
|
||||
where: {
|
||||
uniqueAlbumId: {
|
||||
remoteId: albumRes.album.id,
|
||||
serverId: server.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const scanAlbumDetail = async (
|
||||
server: Server,
|
||||
serverFolder: ServerFolder
|
||||
) => {
|
||||
const promises = [];
|
||||
const dbAlbums = await prisma.album.findMany({
|
||||
where: {
|
||||
serverId: server.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let i = 0; i < dbAlbums.length; i += 1) {
|
||||
promises.push(throttledAlbumFetch(server, serverFolder, dbAlbums[i]));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const scanAll = async (
|
||||
server: Server,
|
||||
serverFolders: ServerFolder[],
|
||||
task: Task
|
||||
) => {
|
||||
queue.scanner.push({
|
||||
fn: async () => {
|
||||
await prisma.task.update({
|
||||
data: { message: 'Beginning scan...' },
|
||||
where: { id: task.id },
|
||||
});
|
||||
|
||||
for (const serverFolder of serverFolders) {
|
||||
await scanGenres(server, task);
|
||||
await scanAlbumArtists(server, serverFolder);
|
||||
await scanAlbums(server, serverFolder);
|
||||
await scanAlbumDetail(server, serverFolder);
|
||||
}
|
||||
|
||||
return { task };
|
||||
},
|
||||
id: task.id,
|
||||
});
|
||||
};
|
||||
|
||||
export const subsonicScanner = {
|
||||
scanAll,
|
||||
scanGenres,
|
||||
};
|
||||
@@ -1,139 +0,0 @@
|
||||
export interface SSBaseResponse {
|
||||
serverVersion?: 'string';
|
||||
status: 'string';
|
||||
type?: 'string';
|
||||
version: 'string';
|
||||
}
|
||||
|
||||
export interface SSMusicFoldersResponse extends SSBaseResponse {
|
||||
musicFolders: {
|
||||
musicFolder: SSMusicFolder[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SSGenresResponse extends SSBaseResponse {
|
||||
genres: {
|
||||
genre: SSGenre[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SSArtistsResponse extends SSBaseResponse {
|
||||
artists: {
|
||||
ignoredArticles: string;
|
||||
index: SSArtistIndex[];
|
||||
lastModified: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SSAlbumListResponse extends SSBaseResponse {
|
||||
albumList2: {
|
||||
album: SSAlbumListEntry[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SSAlbumResponse extends SSBaseResponse {
|
||||
album: SSAlbum;
|
||||
}
|
||||
|
||||
export interface SSArtistInfoResponse extends SSBaseResponse {
|
||||
artistInfo2: SSArtistInfo;
|
||||
}
|
||||
|
||||
export interface SSArtistInfo {
|
||||
biography: string;
|
||||
largeImageUrl?: string;
|
||||
lastFmUrl?: string;
|
||||
mediumImageUrl?: string;
|
||||
musicBrainzId?: string;
|
||||
smallImageUrl?: string;
|
||||
}
|
||||
|
||||
export interface SSMusicFolder {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SSGenre {
|
||||
albumCount?: number;
|
||||
songCount?: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SSArtistIndex {
|
||||
artist: SSArtistListEntry[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SSArtistListEntry {
|
||||
albumCount: string;
|
||||
artistImageUrl?: string;
|
||||
coverArt?: string;
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SSAlbumListEntry {
|
||||
album: string;
|
||||
artist: string;
|
||||
artistId: string;
|
||||
coverArt: string;
|
||||
created: string;
|
||||
duration: number;
|
||||
genre?: string;
|
||||
id: string;
|
||||
isDir: boolean;
|
||||
isVideo: boolean;
|
||||
name: string;
|
||||
parent: string;
|
||||
songCount: number;
|
||||
starred?: boolean;
|
||||
title: string;
|
||||
userRating?: number;
|
||||
year: number;
|
||||
}
|
||||
|
||||
export interface SSAlbum extends SSAlbumListEntry {
|
||||
song: SSSong[];
|
||||
}
|
||||
|
||||
export interface SSSong {
|
||||
album: string;
|
||||
albumId: string;
|
||||
artist: string;
|
||||
artistId?: string;
|
||||
bitRate: number;
|
||||
contentType: string;
|
||||
coverArt: string;
|
||||
created: string;
|
||||
discNumber?: number;
|
||||
duration: number;
|
||||
genre: string;
|
||||
id: string;
|
||||
isDir: boolean;
|
||||
isVideo: boolean;
|
||||
parent: string;
|
||||
path: string;
|
||||
playCount: number;
|
||||
size: number;
|
||||
starred?: boolean;
|
||||
suffix: string;
|
||||
title: string;
|
||||
track: number;
|
||||
type: string;
|
||||
userRating?: number;
|
||||
year: number;
|
||||
}
|
||||
|
||||
export interface SSAlbumsParams {
|
||||
fromYear?: number;
|
||||
genre?: string;
|
||||
musicFolderId?: string;
|
||||
offset?: number;
|
||||
size?: number;
|
||||
toYear?: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SSArtistsParams {
|
||||
musicFolderId?: number;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { ImageType } from '@prisma/client';
|
||||
import { prisma } from '../../lib';
|
||||
import { SSAlbumListEntry, SSAlbumResponse } from './subsonic.types';
|
||||
|
||||
const insertImages = async (items: SSAlbumListEntry[]) => {
|
||||
const createMany = items
|
||||
.filter((item) => item.coverArt)
|
||||
.map((item) => ({
|
||||
remoteUrl: item.coverArt,
|
||||
type: ImageType.PRIMARY,
|
||||
}));
|
||||
|
||||
await prisma.image.createMany({
|
||||
data: createMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
const insertSongImages = async (item: SSAlbumResponse) => {
|
||||
const createMany = item.album.song
|
||||
.filter((song) => song.coverArt)
|
||||
.map((song) => ({
|
||||
remoteUrl: song.coverArt,
|
||||
type: ImageType.PRIMARY,
|
||||
}));
|
||||
|
||||
await prisma.image.createMany({
|
||||
data: createMany,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const subsonicUtils = {
|
||||
insertImages,
|
||||
insertSongImages,
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
import { controller } from '@controllers/index';
|
||||
import { validation, validateRequest } from '@validations/index';
|
||||
|
||||
export const router: Router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', controller.albumArtists.getList);
|
||||
|
||||
router.get(
|
||||
':serverId',
|
||||
validateRequest(validation.albumArtists.detail),
|
||||
controller.albumArtists.getDetail
|
||||
);
|
||||
@@ -1,23 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
import { controller } from '@controllers/index';
|
||||
import { validateRequest, validation } from '@validations/index';
|
||||
|
||||
export const router: Router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
validateRequest(validation.albums.list),
|
||||
controller.albums.getList
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:albumId',
|
||||
validateRequest(validation.albums.detail),
|
||||
controller.albums.getDetail
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:albumId/songs',
|
||||
validateRequest(validation.albums.detail),
|
||||
controller.albums.getDetailSongList
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
import { controller } from '@controllers/index';
|
||||
|
||||
export const router: Router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', controller.artists.getList);
|
||||
|
||||
router.get(':serverId', controller.artists.getDetail);
|
||||
@@ -1,30 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
import passport from 'passport';
|
||||
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',
|
||||
validateRequest(validation.auth.login),
|
||||
passport.authenticate('local'),
|
||||
controller.auth.login
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/register',
|
||||
validateRequest(validation.auth.register),
|
||||
controller.auth.register
|
||||
);
|
||||
|
||||
router.post('/logout', authenticate, controller.auth.logout);
|
||||
|
||||
router.post(
|
||||
'/refresh',
|
||||
validateRequest(validation.auth.refresh),
|
||||
controller.auth.refresh
|
||||
);
|
||||
|
||||
router.get('/ping', controller.auth.ping);
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Router } from 'express';
|
||||
import { helpers } from '../helpers';
|
||||
import { authenticate } from '../middleware';
|
||||
import { router as albumArtistsRouter } from './album-artists.route';
|
||||
import { router as albumsRouter } from './albums.route';
|
||||
import { router as artistsRouter } from './artists.route';
|
||||
import { router as authRouter } from './auth.route';
|
||||
import { router as serversRouter } from './servers.route';
|
||||
import { router as songsRouter } from './songs.route';
|
||||
import { router as tasksRouter } from './tasks.route';
|
||||
import { router as usersRouter } from './users.route';
|
||||
|
||||
export const routes = Router({ mergeParams: true });
|
||||
|
||||
routes.use('/api/auth', authRouter);
|
||||
|
||||
routes.use(authenticate, (_req, _res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
routes.use('/api/tasks', tasksRouter);
|
||||
routes.use('/api/users', usersRouter);
|
||||
routes.use('/api/servers', serversRouter);
|
||||
|
||||
routes.param('serverId', (req, _res, next, 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();
|
||||
});
|
||||
|
||||
routes.use('/api/servers/:serverId/album-artists', albumArtistsRouter);
|
||||
routes.use('/api/servers/:serverId/artists', artistsRouter);
|
||||
routes.use('/api/servers/:serverId/albums', albumsRouter);
|
||||
routes.use('/api/servers/:serverId/songs', songsRouter);
|
||||
@@ -1,87 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
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
|
||||
.route('/')
|
||||
.get(
|
||||
validateRequest(validation.servers.list),
|
||||
controller.servers.getServerList
|
||||
)
|
||||
.post(
|
||||
authenticateAdmin,
|
||||
validateRequest(validation.servers.create),
|
||||
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
|
||||
.route('/:serverId/refresh')
|
||||
.get(
|
||||
authenticateAdmin,
|
||||
validateRequest(validation.servers.refresh),
|
||||
controller.servers.refreshServer
|
||||
);
|
||||
|
||||
router
|
||||
.route('/:serverId/scan')
|
||||
.post(
|
||||
validateRequest(validation.servers.scan),
|
||||
authenticateAdmin,
|
||||
controller.servers.scanServer
|
||||
);
|
||||
|
||||
router
|
||||
.route('/:serverId/url')
|
||||
.post(
|
||||
authenticateAdmin,
|
||||
validateRequest(validation.servers.createUrl),
|
||||
controller.servers.createServerUrl
|
||||
);
|
||||
|
||||
router.param('urlId', async (_req, _res, next, urlId) => {
|
||||
await service.servers.findUrlById({ id: urlId });
|
||||
next();
|
||||
});
|
||||
|
||||
router
|
||||
.route('/:serverId/url/:urlId')
|
||||
.delete(
|
||||
authenticateAdmin,
|
||||
validateRequest(validation.servers.deleteUrl),
|
||||
controller.servers.deleteServerUrl
|
||||
);
|
||||
|
||||
router
|
||||
.route('/:serverId/url/:urlId/enable')
|
||||
.post(
|
||||
validateRequest(validation.servers.enableUrl),
|
||||
controller.servers.enableServerUrl
|
||||
);
|
||||
|
||||
router
|
||||
.route('/:serverId/url/:urlId/disable')
|
||||
.post(
|
||||
validateRequest(validation.servers.disableUrl),
|
||||
controller.servers.disableServerUrl
|
||||
);
|
||||
@@ -1,11 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
import { validation, validateRequest } from '@validations/index';
|
||||
|
||||
export const router: Router = express.Router({ mergeParams: true });
|
||||
|
||||
router.get('/', validateRequest(validation.songs.list), async (req, res) => {
|
||||
// const data = await controller.songs.getSongList(req.authUser, req.query);
|
||||
|
||||
return res.status(200).json({});
|
||||
// return res.status(success.statusCode).json(getSuccessResponse(success));
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
|
||||
export const router: Router = express.Router({ mergeParams: true });
|
||||
|
||||
router.post('/scan', async (_req, res) => {
|
||||
return res.status(200);
|
||||
});
|
||||
|
||||
router.post('/', async (_req, res) => {
|
||||
return res.status(200).json({});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import express, { Router } from 'express';
|
||||
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('/', authenticateAdmin, controller.users.getUserList);
|
||||
|
||||
router.get(
|
||||
':serverId',
|
||||
validateRequest(validation.users.detail),
|
||||
controller.users.getUserDetail
|
||||
);
|
||||
@@ -1,40 +0,0 @@
|
||||
import path from 'path';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import passport from 'passport';
|
||||
import 'express-async-errors';
|
||||
import { errorHandler } from '@/middleware';
|
||||
import { routes } from '@routes/index';
|
||||
|
||||
require('./lib/passport');
|
||||
|
||||
const PORT = 9321;
|
||||
|
||||
const app = express();
|
||||
const staticPath = path.join(__dirname, '../sonixd-client/');
|
||||
|
||||
app.use(express.static(staticPath));
|
||||
app.use(
|
||||
cors({
|
||||
credentials: false,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
||||
origin: [`http://localhost:4343`, `${process.env.APP_BASE_URL}`],
|
||||
})
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
app.get('/', (_req, res) => {
|
||||
res.sendFile(path.join(staticPath, 'index.html'));
|
||||
});
|
||||
|
||||
app.use(routes);
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(9321, () => console.log(`Listening on port ${PORT}`));
|
||||
@@ -1,68 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { albumArtistsService } from './album-artists.service';
|
||||
import { albumsService } from './albums.service';
|
||||
import { artistsService } from './artists.service';
|
||||
import { authService } from './auth.service';
|
||||
import { serversService } from './servers.service';
|
||||
import { usersService } from './users.service';
|
||||
|
||||
export const service = {
|
||||
albumArtists: albumArtistsService,
|
||||
albums: albumsService,
|
||||
artists: artistsService,
|
||||
auth: authService,
|
||||
servers: serversService,
|
||||
users: usersService,
|
||||
};
|
||||
@@ -1,562 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,111 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { OffsetPagination } from '../types/types';
|
||||
|
||||
export interface SongRequestParams extends OffsetPagination {
|
||||
albumIds?: string;
|
||||
artistIds?: string;
|
||||
serverFolderIds: string;
|
||||
serverId: string;
|
||||
songIds?: string;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "es2021"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@controllers/*": ["controllers/*"],
|
||||
"@services/*": ["services/*"],
|
||||
"@validations/*": ["validations/*"],
|
||||
"@middleware/*": ["middleware/*"],
|
||||
"@helpers/*": ["helpers/*"],
|
||||
"@routes/*": ["routes/*"],
|
||||
"@queues/*": ["queues/*"],
|
||||
"@lib/*": ["lib/*"],
|
||||
"@utils/*": ["utils/*"],
|
||||
"@/*": ["*"]
|
||||
},
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strict": true,
|
||||
"pretty": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"typeRoots": ["./types"]
|
||||
},
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
declare module 'express-async-errors';
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
declare namespace Express {
|
||||
export interface Request {
|
||||
authUser: any;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
export enum SortOrder {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
export enum Item {
|
||||
ALBUM = 'album',
|
||||
ALBUM_ARTIST = 'albumArtist',
|
||||
ARTIST = 'artist',
|
||||
FOLDER = 'folder',
|
||||
GENRE = 'genre',
|
||||
PLAYLIST = 'playlist',
|
||||
SONG = 'song',
|
||||
}
|
||||
|
||||
export enum AlbumFilter {
|
||||
FAVORITED,
|
||||
NOT_FAVORITED,
|
||||
}
|
||||
|
||||
export type OffsetPagination = {
|
||||
skip: number;
|
||||
take: number;
|
||||
};
|
||||
|
||||
export type PaginationResponse = {
|
||||
currentPage: number;
|
||||
nextPage: string;
|
||||
prevPage: string;
|
||||
startIndex: number;
|
||||
totalEntries: number;
|
||||
};
|
||||
|
||||
export type SuccessResponse = {
|
||||
data: any;
|
||||
paginationItems?: PaginationItems;
|
||||
};
|
||||
|
||||
export type PaginationItems = {
|
||||
skip: number;
|
||||
take: number;
|
||||
totalEntries: number;
|
||||
url: string;
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
export class ApiError extends Error {
|
||||
message: string;
|
||||
statusCode: number;
|
||||
|
||||
constructor(options: { message: string; statusCode: number }) {
|
||||
super(options.message);
|
||||
this.message = options.message;
|
||||
this.statusCode = options.statusCode;
|
||||
}
|
||||
|
||||
static badRequest(message?: string) {
|
||||
return new ApiError({
|
||||
message: message || 'Bad request.',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
static unauthorized(message?: string) {
|
||||
return new ApiError({
|
||||
message: message || 'Unauthorized.',
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
|
||||
static forbidden(message?: string) {
|
||||
return new ApiError({ message: message || 'Forbidden.', statusCode: 403 });
|
||||
}
|
||||
|
||||
static notFound(message?: string) {
|
||||
return new ApiError({ message: message || 'Not found.', statusCode: 404 });
|
||||
}
|
||||
|
||||
static conflict(message?: string) {
|
||||
return new ApiError({ message: message || 'Conflict.', statusCode: 409 });
|
||||
}
|
||||
|
||||
static gone(message?: string) {
|
||||
return new ApiError({ message: message || 'Gone.', statusCode: 410 });
|
||||
}
|
||||
|
||||
static internal(message?: string) {
|
||||
return new ApiError({
|
||||
message: message || 'Internal error.',
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { PaginationItems, SuccessResponse } from '../types/types';
|
||||
|
||||
export class ApiSuccess {
|
||||
data: any;
|
||||
statusCode: number;
|
||||
paginationItems?: PaginationItems;
|
||||
|
||||
constructor(options: {
|
||||
data: any;
|
||||
paginationItems?: PaginationItems;
|
||||
statusCode: number;
|
||||
}) {
|
||||
this.data = options.data;
|
||||
this.statusCode = options.statusCode;
|
||||
this.paginationItems = options.paginationItems;
|
||||
}
|
||||
|
||||
static ok({ data, paginationItems }: SuccessResponse) {
|
||||
return new ApiSuccess({
|
||||
data,
|
||||
paginationItems,
|
||||
statusCode: 200,
|
||||
});
|
||||
}
|
||||
|
||||
static created({ data, paginationItems }: SuccessResponse) {
|
||||
return new ApiSuccess({ data, paginationItems, statusCode: 201 });
|
||||
}
|
||||
|
||||
static accepted({ data, paginationItems }: SuccessResponse) {
|
||||
return new ApiSuccess({ data, paginationItems, statusCode: 202 });
|
||||
}
|
||||
|
||||
static noContent({ data, paginationItems }: SuccessResponse) {
|
||||
return new ApiSuccess({ data, paginationItems, statusCode: 204 });
|
||||
}
|
||||
|
||||
static resetContent({ data, paginationItems }: SuccessResponse) {
|
||||
return new ApiSuccess({ data, paginationItems, statusCode: 205 });
|
||||
}
|
||||
|
||||
static partialContent({ data, paginationItems }: SuccessResponse) {
|
||||
return new ApiSuccess({ data, paginationItems, statusCode: 206 });
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { User } from '@prisma/client';
|
||||
import { prisma } from '../lib';
|
||||
|
||||
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: [
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
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: [
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
serverFolderPermissions: {
|
||||
some: { userId: { equals: user.id } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const serverFoldersWithAccessIds = serverFoldersWithAccess.map(
|
||||
(serverFolder) => serverFolder.id
|
||||
);
|
||||
|
||||
return serverFoldersWithAccessIds;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { PaginationItems } from '../types/types';
|
||||
|
||||
const getPaginationUrl = (
|
||||
url: string,
|
||||
skip: number,
|
||||
take: number,
|
||||
action: 'next' | 'prev'
|
||||
) => {
|
||||
if (action === 'next') {
|
||||
return url.replace(/skip=(\d+)/gm, `skip=${skip + take}`);
|
||||
}
|
||||
|
||||
return url.replace(/skip=(\d+)/gm, `skip=${skip - take}`);
|
||||
};
|
||||
|
||||
export const getSuccessResponse = (options: {
|
||||
data: any;
|
||||
paginationItems?: PaginationItems;
|
||||
statusCode: number;
|
||||
}) => {
|
||||
const { statusCode, data, paginationItems } = options;
|
||||
|
||||
let pagination;
|
||||
|
||||
if (paginationItems) {
|
||||
const { skip, totalEntries, take, url } = paginationItems;
|
||||
|
||||
const hasPrevPage = skip - take >= 0;
|
||||
const hasNextPage = skip + take <= totalEntries;
|
||||
|
||||
pagination = {
|
||||
nextPage: hasNextPage ? getPaginationUrl(url, skip, take, 'next') : null,
|
||||
prevPage: hasPrevPage ? getPaginationUrl(url, skip, take, 'prev') : null,
|
||||
skip,
|
||||
totalEntries,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
pagination,
|
||||
response: 'Success',
|
||||
statusCode,
|
||||
};
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
export const groupByProperty = (object: any, property: string) => {
|
||||
return object.reduce((groups: any, item: any) => {
|
||||
const group = groups[item[property]] || [];
|
||||
group.push(item);
|
||||
groups[item[property]] = group;
|
||||
return groups;
|
||||
}, {});
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export * from './api-error';
|
||||
export * from './api-success';
|
||||
export * from './get-success-response';
|
||||
export * from './group-by-property';
|
||||
export * from './split-number-string';
|
||||
export * from './split-text-string';
|
||||
export * from './folder-permissions';
|
||||
export * from './is-array-equal';
|
||||
export * from './is-json-string';
|
||||
export * from './unique-array';
|
||||
export * from './random-string';
|
||||
@@ -1,13 +0,0 @@
|
||||
export const validateArrayEqual = (array1: any[], array2: any[]) => {
|
||||
if (array1.length === array2.length) {
|
||||
return array1.every((element) => {
|
||||
if (array2.includes(element)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
export const isJsonString = (string: string) => {
|
||||
try {
|
||||
JSON.parse(string);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
export const randomString = (length: number) => {
|
||||
const charSet =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let string = '';
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
const randomPoz = Math.floor(Math.random() * charSet.length);
|
||||
string += charSet.substring(randomPoz, randomPoz + 1);
|
||||
}
|
||||
return string;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
export const splitNumberString = (string?: string, delimiter = ',') => {
|
||||
if (!string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return string.split(delimiter).map((s: string) => {
|
||||
return Number(s);
|
||||
});
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export const splitTextString = (string: string, delimiter = ',') => {
|
||||
return string.split(delimiter).map((s: string) => {
|
||||
return String(s);
|
||||
});
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const uniqueArray = (value: any, index: any, self: any) => {
|
||||
return self.indexOf(value) === index && value !== undefined;
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { idValidation, paginationValidation } from './shared.validation';
|
||||
|
||||
export const list = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({
|
||||
...paginationValidation,
|
||||
serverFolderIds: z.string().min(1),
|
||||
}),
|
||||
};
|
||||
|
||||
export const detail = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('id') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
export const albumArtistsValidation = {
|
||||
detail,
|
||||
list,
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { AlbumSort } from '@helpers/albums.helpers';
|
||||
import {
|
||||
idValidation,
|
||||
orderByValidation,
|
||||
paginationValidation,
|
||||
serverFolderIdValidation,
|
||||
serverUrlIdValidation,
|
||||
} from './shared.validation';
|
||||
|
||||
const list = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({
|
||||
...paginationValidation,
|
||||
...serverFolderIdValidation,
|
||||
...orderByValidation,
|
||||
...serverUrlIdValidation,
|
||||
sortBy: z.nativeEnum(AlbumSort),
|
||||
}),
|
||||
};
|
||||
|
||||
const detail = {
|
||||
body: z.object({}),
|
||||
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,30 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
import { AlbumSort } from '@helpers/albums.helpers';
|
||||
import {
|
||||
idValidation,
|
||||
orderByValidation,
|
||||
paginationValidation,
|
||||
serverFolderIdValidation,
|
||||
} from './shared.validation';
|
||||
|
||||
const list = {
|
||||
body: z.object({}),
|
||||
params: z.object({}),
|
||||
query: z.object({
|
||||
...paginationValidation,
|
||||
...serverFolderIdValidation,
|
||||
...orderByValidation,
|
||||
sortBy: z.nativeEnum(AlbumSort),
|
||||
}),
|
||||
};
|
||||
|
||||
const detail = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('id') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
export const artistsValidation = {
|
||||
detail,
|
||||
list,
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const login = {
|
||||
body: z.object({
|
||||
password: z.string(),
|
||||
username: z.string(),
|
||||
}),
|
||||
params: z.object({}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const register = {
|
||||
body: z.object({
|
||||
password: z.string().min(6).max(255),
|
||||
username: z.string().min(4).max(26),
|
||||
}),
|
||||
params: z.object({}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const refresh = {
|
||||
body: z.object({
|
||||
refreshToken: z.string(),
|
||||
}),
|
||||
params: z.object({}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
export const authValidation = {
|
||||
login,
|
||||
refresh,
|
||||
register,
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { albumArtistsValidation } from './album-artists.validation';
|
||||
import { albumsValidation } from './albums.validation';
|
||||
import { artistsValidation } from './artists.validation';
|
||||
import { authValidation } from './auth.validation';
|
||||
import { serversValidation } from './servers.validation';
|
||||
import { songsValidation } from './songs.validation';
|
||||
import { usersValidation } from './users.validation';
|
||||
|
||||
export { validateRequest, TypedRequest } from './shared.validation';
|
||||
|
||||
export const validation = {
|
||||
albumArtists: albumArtistsValidation,
|
||||
albums: albumsValidation,
|
||||
artists: artistsValidation,
|
||||
auth: authValidation,
|
||||
servers: serversValidation,
|
||||
songs: songsValidation,
|
||||
users: usersValidation,
|
||||
};
|
||||
@@ -1,154 +0,0 @@
|
||||
import { ServerType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
import { idValidation } from './shared.validation';
|
||||
|
||||
const detail = {
|
||||
body: z.object({}),
|
||||
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({}),
|
||||
};
|
||||
|
||||
const create = {
|
||||
body: z.object({
|
||||
legacy: z.boolean().optional(),
|
||||
name: z.string(),
|
||||
password: z.string(),
|
||||
type: z.enum([
|
||||
ServerType.JELLYFIN,
|
||||
ServerType.SUBSONIC,
|
||||
ServerType.NAVIDROME,
|
||||
]),
|
||||
url: z.string(),
|
||||
username: z.string(),
|
||||
}),
|
||||
params: z.object({}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const scan = {
|
||||
body: z.object({ serverFolderId: z.string().array().optional() }),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const refresh = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const createCredential = {
|
||||
body: z.object({ credential: z.string(), username: z.string() }),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const getCredentialDetail = {
|
||||
body: z.object({}),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const deleteCredential = {
|
||||
body: z.object({}),
|
||||
params: z.object({
|
||||
...idValidation('serverId'),
|
||||
...idValidation('credentialId'),
|
||||
}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const enableCredential = {
|
||||
body: z.object({}),
|
||||
params: z.object({
|
||||
...idValidation('serverId'),
|
||||
...idValidation('credentialId'),
|
||||
}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const disableCredential = {
|
||||
body: z.object({}),
|
||||
params: z.object({
|
||||
...idValidation('serverId'),
|
||||
...idValidation('credentialId'),
|
||||
}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const createUrl = {
|
||||
body: z.object({ url: z.string() }),
|
||||
params: z.object({ ...idValidation('serverId') }),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const deleteUrl = {
|
||||
body: z.object({}),
|
||||
params: z.object({
|
||||
...idValidation('serverId'),
|
||||
...idValidation('urlId'),
|
||||
}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const enableUrl = {
|
||||
body: z.object({}),
|
||||
params: z.object({
|
||||
...idValidation('serverId'),
|
||||
...idValidation('urlId'),
|
||||
}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
const disableUrl = {
|
||||
body: z.object({}),
|
||||
params: z.object({
|
||||
...idValidation('serverId'),
|
||||
...idValidation('urlId'),
|
||||
}),
|
||||
query: z.object({}),
|
||||
};
|
||||
|
||||
export const serversValidation = {
|
||||
create,
|
||||
createCredential,
|
||||
createUrl,
|
||||
deleteCredential,
|
||||
deleteServer,
|
||||
deleteUrl,
|
||||
detail,
|
||||
disableCredential,
|
||||
disableUrl,
|
||||
enableCredential,
|
||||
enableUrl,
|
||||
getCredentialDetail,
|
||||
list,
|
||||
refresh,
|
||||
scan,
|
||||
update,
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
import { Request, RequestHandler } from 'express';
|
||||
import { z, ZodError, ZodSchema } from 'zod';
|
||||
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 {
|
||||
body: z.AnyZodObject;
|
||||
params: z.AnyZodObject;
|
||||
query: z.AnyZodObject;
|
||||
}
|
||||
> = Request<z.infer<S['params']>, any, z.infer<S['body']>, z.infer<S['query']>>;
|
||||
|
||||
export enum ValidationType {
|
||||
BODY = 'Body',
|
||||
PARAMS = 'Params',
|
||||
QUERY = 'Query',
|
||||
}
|
||||
|
||||
type RequestValidation<TParams, TQuery, TBody> = {
|
||||
body?: ZodSchema<TBody>;
|
||||
params?: ZodSchema<TParams>;
|
||||
query?: ZodSchema<TQuery>;
|
||||
};
|
||||
|
||||
type ErrorListItem = {
|
||||
errors: ZodError<any>;
|
||||
type: ValidationType;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const getErrorValue = (object: any, error: ZodError<any>) => {
|
||||
const e = error.errors[0];
|
||||
if (e.code === z.ZodIssueCode.invalid_type) {
|
||||
return e.expected;
|
||||
}
|
||||
|
||||
return object[e.path[0]][e.path[1]];
|
||||
};
|
||||
|
||||
export const validateRequest: <TParams = any, TQuery = any, TBody = any>(
|
||||
schemas: RequestValidation<TParams, TQuery, TBody>
|
||||
) => RequestHandler<TParams, any, TBody, TQuery> =
|
||||
({ params, query, body }) =>
|
||||
(req, _res, next) => {
|
||||
const errors: Array<ErrorListItem> = [];
|
||||
if (params) {
|
||||
const parsed = params.safeParse(req.params);
|
||||
if (!parsed.success) {
|
||||
errors.push({
|
||||
errors: parsed.error,
|
||||
type: ValidationType.PARAMS,
|
||||
value: getErrorValue(req.params, parsed.error),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (query) {
|
||||
const parsed = query.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
const value = getErrorValue(req.query, parsed.error);
|
||||
errors.push({
|
||||
errors: parsed.error,
|
||||
type: ValidationType.QUERY,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (body) {
|
||||
const parsed = body.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
errors.push({
|
||||
errors: parsed.error,
|
||||
type: ValidationType.BODY,
|
||||
value: getErrorValue(req.body, parsed.error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
const message = JSON.stringify(
|
||||
[
|
||||
// `(${errors[0].type})`,
|
||||
`(${errors[0].type}: ${errors[0].errors.issues[0].path[0]})`,
|
||||
errors[0].value && `[${errors[0].value}]`,
|
||||
errors[0].errors.issues[0].message,
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join(' ')
|
||||
);
|
||||
|
||||
throw ApiError.badRequest(message);
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
// const requiredErrorMessage = (
|
||||
// type: 'Query' | 'Body' | 'Params',
|
||||
// key: string
|
||||
// ) => {
|
||||
// return `(${type}) [${key}] Required`;
|
||||
// };
|
||||
|
||||
export const paginationValidation = {
|
||||
skip: z.string().refine((value) => {
|
||||
const parsed = Number(value);
|
||||
return !Number.isNaN(parsed) && parsed >= 0;
|
||||
}),
|
||||
take: z.string().refine((value) => {
|
||||
const parsed = Number(value);
|
||||
return !Number.isNaN(parsed) && parsed >= 0;
|
||||
}),
|
||||
};
|
||||
|
||||
export const serverUrlIdValidation = {
|
||||
serverUrlId: z.optional(z.string().uuid()),
|
||||
};
|
||||
|
||||
export const idValidation = (property: string) => {
|
||||
return { [property]: z.string().uuid() };
|
||||
};
|
||||
|
||||
export const serverFolderIdValidation = {
|
||||
serverFolderId: z.optional(z.string().uuid().array()),
|
||||
};
|
||||
|
||||
export const orderByValidation = {
|
||||
orderBy: z.nativeEnum(SortOrder),
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user