Move server directory outside of frontend src

This commit is contained in:
jeffvli
2022-10-25 16:52:45 -07:00
parent 863dce88b7
commit 0438f2d5f2
105 changed files with 16946 additions and 6901 deletions
+7
View File
@@ -0,0 +1,7 @@
import { subsonicApi } from './subsonic.api';
import { subsonicScanner } from './subsonic.scanner';
export const subsonic = {
api: subsonicApi,
scanner: subsonicScanner,
};
+157
View File
@@ -0,0 +1,157 @@
import { Server } from '@prisma/client';
import { randomString } from '@utils/index';
import axios from 'axios';
import md5 from 'md5';
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=Feishin&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=Feishin&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=Feishin&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=Feishin&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=Feishin&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=Feishin&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=Feishin&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,
};
+288
View File
@@ -0,0 +1,288 @@
/* 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,
};
+139
View File
@@ -0,0 +1,139 @@
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;
}
+36
View File
@@ -0,0 +1,36 @@
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,
};