Have reasonable batch linking working for YouTube.

editsong
Sander Vocke 5 years ago
parent 342c4f0579
commit 02f6ea1a84
  1. 21
      client/src/api.ts
  2. 112
      client/src/components/windows/manage_links/BatchLinkDialog.tsx
  3. 19
      client/src/lib/backend/queries.tsx
  4. 153
      client/src/lib/integration/youtubemusic/YoutubeMusicWebScraper.tsx
  5. 572
      server/endpoints/Album.ts
  6. 13
      server/endpoints/Artist.ts
  7. 464
      server/endpoints/Song.ts

@ -159,9 +159,9 @@ export interface SongDetailsRequest { }
export interface SongDetailsResponse {
title: string,
storeLinks: string[],
artistIds: number[],
albumIds: number[],
tagIds: number[],
artists?: ArtistDetailsResponseWithId[],
albums?: AlbumDetailsResponseWithId[],
tags?: TagDetailsResponseWithId[],
}
export function checkSongDetailsRequest(req: any): boolean {
return true;
@ -172,7 +172,7 @@ export const ArtistDetailsEndpoint = '/artist/:id';
export interface ArtistDetailsRequest { }
export interface ArtistDetailsResponse {
name: string,
tagIds: number[],
tags?: TagDetailsResponseWithId[],
storeLinks: string[],
}
export function checkArtistDetailsRequest(req: any): boolean {
@ -244,9 +244,9 @@ export const AlbumDetailsEndpoint = '/album/:id';
export interface AlbumDetailsRequest { }
export interface AlbumDetailsResponse {
name: string;
tagIds: number[];
artistIds: number[];
songIds: number[];
artists?: ArtistDetailsResponseWithId[],
songs?: SongDetailsResponseWithId[],
tags?: TagDetailsResponseWithId[],
storeLinks: string[];
}
export function checkAlbumDetailsRequest(req: any): boolean {
@ -468,4 +468,9 @@ export interface DeleteIntegrationRequest { }
export interface DeleteIntegrationResponse { }
export function checkDeleteIntegrationRequest(req: any): boolean {
return true;
}
}
export interface ArtistDetailsResponseWithId extends ArtistDetailsResponse { id: number }
export interface AlbumDetailsResponseWithId extends AlbumDetailsResponse { id: number }
export interface TagDetailsResponseWithId extends TagDetailsResponse { id: number }
export interface SongDetailsResponseWithId extends SongDetailsResponse { id: number }

@ -1,5 +1,5 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { Box, Button, Checkbox, createStyles, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControlLabel, List, ListItem, ListItemIcon, ListItemText, makeStyles, MenuItem, Paper, Select, Theme, Typography } from "@material-ui/core";
import { Box, Button, Checkbox, createStyles, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControlLabel, LinearProgress, List, ListItem, ListItemIcon, ListItemText, makeStyles, MenuItem, Paper, Select, Theme, Typography } from "@material-ui/core";
import StoreLinkIcon from '../../common/StoreLinkIcon';
import { $enum } from 'ts-enum-util';
import { IntegrationState, useIntegrations } from '../../../lib/integration/useIntegrations';
@ -11,6 +11,7 @@ import asyncPool from "tiny-async-pool";
import { getSong } from '../../../lib/backend/songs';
import { getAlbum } from '../../../lib/backend/albums';
import { getArtist } from '../../../lib/backend/artists';
import { modifyAlbum, modifyArtist, modifySong } from '../../../lib/saveChanges';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@ -24,6 +25,7 @@ enum BatchJobState {
Idle = 0,
Collecting,
Running,
Finished,
}
interface Task {
@ -99,11 +101,13 @@ async function doLinking(
console.log("Linking start!", toLink);
// Start the collecting phase.
setStatus({
state: BatchJobState.Collecting,
numTasks: 0,
tasksSuccess: 0,
tasksFailed: 0,
setStatus((s: any) => {
return {
state: BatchJobState.Collecting,
numTasks: 0,
tasksSuccess: 0,
tasksFailed: 0,
}
});
console.log("Starting collection");
@ -127,17 +131,30 @@ async function doLinking(
console.log("Done collecting.", tasks)
// Start the linking phase.
setStatus((status: BatchJobStatus) => {
status.state = BatchJobState.Running;
status.numTasks = tasks.length;
console.log("Collected status:", status)
return status;
return {
...status,
state: BatchJobState.Running,
numTasks: tasks.length
}
});
let makeJob: (t: Task) => Promise<void> = (t: Task) => {
let integration = integrations.find((i: IntegrationState) => i.id === t.integrationId);
return (async () => {
let onSuccess = () => setStatus((s: BatchJobStatus) => { s.tasksSuccess += 1; return s; });
let onFail = () => setStatus((s: BatchJobStatus) => { s.tasksFailed += 1; return s; });
let onSuccess = () =>
setStatus((s: BatchJobStatus) => {
return {
...s,
tasksSuccess: s.tasksSuccess + 1,
}
});
let onFail = () =>
setStatus((s: BatchJobStatus) => {
return {
...s,
tasksFailed: s.tasksFailed + 1,
}
});
try {
if (integration === undefined) { return; }
console.log('integration search:', integration)
@ -154,18 +171,38 @@ async function doLinking(
[ItemType.Artist]: getArtist,
}
let queryFuncs: any = {
[ItemType.Song]: (s: any) => `${s.title}`,
[ItemType.Album]: (s: any) => `${s.name}`,
[ItemType.Song]: (s: any) => `${s.title}` +
`${s.artists && s.artists.length > 0 && ` ${s.artists[0].name}`}` +
`${s.albums && s.albums.length > 0 && ` ${s.albums[0].name}`}`,
[ItemType.Album]: (s: any) => `${s.name}` +
`${s.artists && s.artists.length > 0 && ` ${s.artists[0].name}`}`,
[ItemType.Artist]: (s: any) => `${s.name}`,
}
let query = queryFuncs[t.itemType](await getFuncs[t.itemType](t.itemId));
let modifyFuncs: any = {
[ItemType.Song]: modifySong,
[ItemType.Album]: modifyAlbum,
[ItemType.Artist]: modifyArtist,
}
let item = await getFuncs[t.itemType](t.itemId);
let query = queryFuncs[t.itemType](item);
let candidates = await searchFuncs[t.itemType](
query,
1,
);
var success = false;
if (candidates && candidates.length && candidates.length > 0 && candidates[0].url) {
await modifyFuncs[t.itemType](
t.itemId,
{
storeLinks: [...item.storeLinks, candidates[0].url]
}
)
success = true;
}
console.log(query, candidates);
if (candidates && candidates.length && candidates.length > 0) {
if (success) {
onSuccess();
} else {
onFail();
@ -178,13 +215,14 @@ async function doLinking(
})();
}
await asyncPool(4, tasks, makeJob);
await asyncPool(8, tasks, makeJob);
// Finalize.
setStatus((status: BatchJobStatus) => {
status.state = BatchJobState.Idle;
console.log("Done running:", status)
return status;
return {
...status,
state: BatchJobState.Finished,
}
});
}
@ -193,16 +231,31 @@ function ProgressDialog(props: {
onClose: () => void,
status: BatchJobStatus,
}) {
let donePercent = ((props.status.tasksFailed + props.status.tasksSuccess) / (props.status.numTasks || 1)) * 100;
return <Dialog
open={props.open}
onClose={props.onClose}
>
<DialogTitle>Batch linking in progress...</DialogTitle>
{props.status.state === BatchJobState.Collecting || props.status.state === BatchJobState.Running &&
<DialogTitle>Batch linking in progress...</DialogTitle>}
{props.status.state === BatchJobState.Finished &&
<DialogTitle>Batch linking finished</DialogTitle>}
<DialogContent>
<DialogContentText>
Closing or refreshing this page will interrupt and abort the process.
</DialogContentText>
{props.status.state === BatchJobState.Collecting || props.status.state === BatchJobState.Running &&
<DialogContentText>
Closing or refreshing this page will interrupt and abort the process.
</DialogContentText>}
<Box minWidth="200px"><LinearProgress variant="determinate" color="secondary" value={donePercent} /></Box>
<Typography>
Found: {props.status.tasksSuccess}<br />
Failed: {props.status.tasksFailed}<br />
Total: {props.status.numTasks}<br />
</Typography>
</DialogContent>
{props.status.state === BatchJobState.Finished && <DialogActions>
<Button variant="contained" onClick={props.onClose}>Done</Button>
</DialogActions>}
</Dialog>
}
@ -380,8 +433,15 @@ export default function BatchLinkDialog(props: {
}}
/>
<ProgressDialog
open={jobStatus.state === BatchJobState.Collecting || jobStatus.state === BatchJobState.Running}
onClose={() => { }}
open={jobStatus.state === BatchJobState.Collecting || jobStatus.state === BatchJobState.Running || jobStatus.state === BatchJobState.Finished}
onClose={() => {
setJobStatus({
numTasks: 0,
tasksSuccess: 0,
tasksFailed: 0,
state: BatchJobState.Idle,
})
}}
status={jobStatus}
/>
</>

@ -24,17 +24,18 @@ export async function queryItems(
tags: number,
songs: number,
}> {
console.log("Types:", types);
var q: serverApi.QueryRequest = {
query: query ? toApiQuery(query) : {},
offsetsLimits: {
artistOffset: (serverApi.ItemType.Artist in types) ? (offset || 0) : undefined,
artistLimit: (serverApi.ItemType.Artist in types) ? (limit || -1) : undefined,
albumOffset: (serverApi.ItemType.Album in types) ? (offset || 0) : undefined,
albumLimit: (serverApi.ItemType.Album in types) ? (limit || -1) : undefined,
songOffset: (serverApi.ItemType.Song in types) ? (offset || 0) : undefined,
songLimit: (serverApi.ItemType.Song in types) ? (limit || -1) : undefined,
tagOffset: (serverApi.ItemType.Tag in types) ? (offset || 0) : undefined,
tagLimit: (serverApi.ItemType.Tag in types) ? (limit || -1) : undefined,
artistOffset: (types.includes(serverApi.ItemType.Artist)) ? (offset || 0) : undefined,
artistLimit: (types.includes(serverApi.ItemType.Artist)) ? (limit || -1) : undefined,
albumOffset: (types.includes(serverApi.ItemType.Album)) ? (offset || 0) : undefined,
albumLimit: (types.includes(serverApi.ItemType.Album)) ? (limit || -1) : undefined,
songOffset: (types.includes(serverApi.ItemType.Song)) ? (offset || 0) : undefined,
songLimit: (types.includes(serverApi.ItemType.Song)) ? (limit || -1) : undefined,
tagOffset: (types.includes(serverApi.ItemType.Tag)) ? (offset || 0) : undefined,
tagLimit: (types.includes(serverApi.ItemType.Tag)) ? (limit || -1) : undefined,
},
ordering: {
orderBy: {
@ -45,6 +46,8 @@ export async function queryItems(
responseType: responseType,
};
console.log(q);
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },

@ -35,7 +35,7 @@ export function extractInitialData(text: string): any | undefined {
export function parseSongs(initialData: any): IntegrationSong[] {
try {
var songMusicResponsiveListItemRenderers: any[] = [];
var musicResponsiveListItemRenderers: any[] = [];
// Scrape for any "Song"-type items.
initialData.contents.sectionListRenderer.contents.forEach((c: any) => {
@ -45,13 +45,13 @@ export function parseSongs(initialData: any): IntegrationSong[] {
cc.musicResponsiveListItemRenderer.flexColumns &&
cc.musicResponsiveListItemRenderer.flexColumns[1]
.musicResponsiveListItemFlexColumnRenderer.text.runs[0].text === "Song") {
songMusicResponsiveListItemRenderers.push(cc.musicResponsiveListItemRenderer);
}
musicResponsiveListItemRenderers.push(cc.musicResponsiveListItemRenderer);
}
})
}
})
return songMusicResponsiveListItemRenderers.map((s: any) => {
return musicResponsiveListItemRenderers.map((s: any) => {
let videoId = s.doubleTapCommand.watchEndpoint.videoId;
let columns = s.flexColumns;
@ -94,6 +94,82 @@ export function parseSongs(initialData: any): IntegrationSong[] {
}
}
export function parseArtists(initialData: any): IntegrationSong[] {
try {
var musicResponsiveListItemRenderers: any[] = [];
// Scrape for any "Artist"-type items.
initialData.contents.sectionListRenderer.contents.forEach((c: any) => {
if (c.musicShelfRenderer) {
c.musicShelfRenderer.contents.forEach((cc: any) => {
if (cc.musicResponsiveListItemRenderer &&
cc.musicResponsiveListItemRenderer.flexColumns &&
cc.musicResponsiveListItemRenderer.flexColumns[1]
.musicResponsiveListItemFlexColumnRenderer.text.runs[0].text === "Artist") {
musicResponsiveListItemRenderers.push(cc.musicResponsiveListItemRenderer);
}
})
}
})
return musicResponsiveListItemRenderers.map((s: any) => {
let browseId = s.navigationEndpoint.browseEndpoint.browseId;
let columns = s.flexColumns;
if (columns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text !== "Artist") {
throw new Error('artist item doesnt match scraper expectation');
}
let name = columns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text;
return {
name: name,
url: `https://music.youtube.com/browse/${browseId}`,
}
})
} catch (e) {
console.log("Error parsing artists:", e.message);
return [];
}
}
export function parseAlbums(initialData: any): IntegrationSong[] {
try {
var musicResponsiveListItemRenderers: any[] = [];
// Scrape for any "Artist"-type items.
initialData.contents.sectionListRenderer.contents.forEach((c: any) => {
if (c.musicShelfRenderer) {
c.musicShelfRenderer.contents.forEach((cc: any) => {
if (cc.musicResponsiveListItemRenderer &&
cc.musicResponsiveListItemRenderer.flexColumns &&
["Album", "Single"].includes(cc.musicResponsiveListItemRenderer.flexColumns[1]
.musicResponsiveListItemFlexColumnRenderer.text.runs[0].text)) {
musicResponsiveListItemRenderers.push(cc.musicResponsiveListItemRenderer);
}
})
}
})
return musicResponsiveListItemRenderers.map((s: any) => {
let browseId = s.navigationEndpoint.browseEndpoint.browseId;
let columns = s.flexColumns;
if (!["Album", "Single"].includes(columns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text)) {
throw new Error('album item doesnt match scraper expectation');
}
let name = columns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text;
return {
name: name,
url: `https://music.youtube.com/browse/${browseId}`,
}
})
} catch (e) {
console.log("Error parsing albums:", e.message);
return [];
}
}
export default class YoutubeMusicWebScraper extends Integration {
integrationId: number;
@ -127,8 +203,6 @@ export default class YoutubeMusicWebScraper extends Integration {
let text = await response.text();
let songs = parseSongs(extractInitialData(text));
console.log("Found songs", songs);
if (!Array.isArray(songs) || songs.length === 0 || songs[0].title !== "No One Knows") {
throw new Error("Test failed; No One Knows was not correctly identified.");
}
@ -142,61 +216,20 @@ export default class YoutubeMusicWebScraper extends Integration {
let text = await response.text();
return parseSongs(extractInitialData(text));
}
async searchAlbum(query: string, limit: number): Promise<IntegrationAlbum[]> { return []; }
async searchArtist(query: string, limit: number): Promise<IntegrationArtist[]> { return []; }
async searchAlbum(query: string, limit: number): Promise<IntegrationAlbum[]> {
const response = await fetch(
(process.env.REACT_APP_BACKEND || "") +
`/integrations/${this.integrationId}/search?q=${encodeURIComponent(query)}`);
async search(query: string, type: SearchType, limit: number):
Promise<IntegrationSong[] | IntegrationAlbum[] | IntegrationArtist[]> {
let text = await response.text();
return parseAlbums(extractInitialData(text));
}
async searchArtist(query: string, limit: number): Promise<IntegrationArtist[]> {
const response = await fetch(
(process.env.REACT_APP_BACKEND || "") +
`/integrations/${this.integrationId}/search?q=${encodeURIComponent(query)}`);
return [];
// const response = await fetch(
// (process.env.REACT_APP_BACKEND || "") +
// `/integrations/${this.integrationId}/v1/search?q=${encodeURIComponent(query)}&type=${type}&limit=${limit}`);
// if (!response.ok) {
// throw new Error("Spotify Client Credentials search failed: " + JSON.stringify(response));
// }
// let json = await response.json();
// console.log("Response:", json);
// switch(type) {
// case SearchType.Song: {
// return json.tracks.items.map((r: any): IntegrationSong => {
// return {
// title: r.name,
// url: r.external_urls.spotify,
// artist: {
// name: r.artists && r.artists[0].name,
// url: r.artists && r.artists[0].external_urls.spotify,
// },
// album: {
// name: r.album && r.album.name,
// url: r.album && r.album.external_urls.spotify,
// }
// }
// })
// }
// case SearchType.Artist: {
// return json.artists.items.map((r: any): IntegrationArtist => {
// return {
// name: r.name,
// url: r.external_urls.spotify,
// }
// })
// }
// case SearchType.Album: {
// return json.albums.items.map((r: any): IntegrationAlbum => {
// return {
// name: r.name,
// url: r.external_urls.spotify,
// artist: {
// name: r.artists[0].name,
// url: r.artists[0].external_urls.spotify,
// },
// }
// })
// }
let text = await response.text();
return parseArtists(extractInitialData(text));
}
}

@ -4,299 +4,311 @@ import Knex from 'knex';
import asJson from '../lib/asJson';
export const GetAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkAlbumDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid GetAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
if (!api.checkAlbumDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid GetAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const { id: userId } = req.user;
const { id: userId } = req.user;
try {
// Start transfers for songs, tags and artists.
// Also request the album itself.
const tagIdsPromise = knex.select('tagId')
.from('albums_tags')
.where({ 'albumId': req.params.id })
.then((tags: any) => {
return tags.map((tag: any) => tag['tagId'])
});
const songIdsPromise = knex.select('songId')
.from('songs_albums')
.where({ 'albumId': req.params.id })
.then((songs: any) => {
return songs.map((song: any) => song['songId'])
});
const artistIdsPromise = knex.select('artistId')
.from('artists_albums')
.where({ 'albumId': req.params.id })
.then((artists: any) => {
return artists.map((artist: any) => artist['artistId'])
});
const albumPromise = knex.select('name', 'storeLinks')
.from('albums')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((albums: any) => albums[0]);
// Wait for the requests to finish.
const [album, tags, songs, artists] =
await Promise.all([albumPromise, tagIdsPromise, songIdsPromise, artistIdsPromise]);
// Respond to the request.
if (album) {
const response: api.AlbumDetailsResponse = {
name: album['name'],
artistIds: artists,
tagIds: tags,
songIds: songs,
storeLinks: asJson(album['storeLinks']),
};
await res.send(response);
} else {
await res.status(404).send({});
}
} catch (e) {
try {
// Start transfers for songs, tags and artists.
// Also request the album itself.
const tagsPromise: Promise<api.TagDetailsResponseWithId[]> = knex.select('tagId')
.from('albums_tags')
.where({ 'albumId': req.params.id })
.then((tags: any) => {
return tags.map((tag: any) => tag['tagId'])
})
.then((ids: number[]) => knex.select(['id', 'name', 'parentId'])
.from('tags')
.whereIn('id', ids));
const songsPromise: Promise<api.SongDetailsResponseWithId[]> = knex.select('songId')
.from('songs_albums')
.where({ 'albumId': req.params.id })
.then((songs: any) => {
return songs.map((song: any) => song['songId'])
})
.then((ids: number[]) => knex.select(['id', 'title', 'storeLinks'])
.from('songs')
.whereIn('id', ids));
const artistsPromise = knex.select('artistId')
.from('artists_albums')
.where({ 'albumId': req.params.id })
.then((artists: any) => {
return artists.map((artist: any) => artist['artistId'])
})
.then((ids: number[]) => knex.select(['id', 'name', 'storeLinks'])
.from('artists')
.whereIn('id', ids));
const albumPromise = knex.select('name', 'storeLinks')
.from('albums')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((albums: any) => albums[0]);
// Wait for the requests to finish.
const [album, tags, songs, artists] =
await Promise.all([albumPromise, tagsPromise, songsPromise, artistsPromise]);
// Respond to the request.
if (album) {
const response: api.AlbumDetailsResponse = {
name: album['name'],
artists: artists,
tags: tags,
songs: songs,
storeLinks: asJson(album['storeLinks']),
};
await res.send(response);
} else {
await res.status(404).send({});
}
} catch (e) {
catchUnhandledErrors(e);
}
}
}
export const PostAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkCreateAlbumRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid PostAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.CreateAlbumRequest = req.body;
const { id: userId } = req.user;
console.log("User ", userId, ": Post Album ", reqObject);
await knex.transaction(async (trx) => {
try {
// Start retrieving artists.
const artistIdsPromise = reqObject.artistIds ?
trx.select('id')
.from('artists')
.where({ 'user': userId })
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('tags')
.where({ 'user': userId })
.whereIn('id', reqObject.tagIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Wait for the requests to finish.
var [artists, tags] = await Promise.all([artistIdsPromise, tagIdsPromise]);;
// Check that we found all artists and tags we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) {
const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
// Create the album.
const albumId = (await trx('albums')
.insert({
name: reqObject.name,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
user: userId,
})
.returning('id') // Needed for Postgres
)[0];
// Link the artists via the linking table.
if (artists && artists.length) {
await trx('artists_albums').insert(
artists.map((artistId: number) => {
return {
artistId: artistId,
albumId: albumId,
}
})
)
}
// Link the tags via the linking table.
if (tags && tags.length) {
await trx('albums_tags').insert(
tags.map((tagId: number) => {
return {
albumId: albumId,
tagId: tagId,
}
})
)
}
// Respond to the request.
const responseObject: api.CreateSongResponse = {
id: albumId
if (!api.checkCreateAlbumRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid PostAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.CreateAlbumRequest = req.body;
const { id: userId } = req.user;
console.log("User ", userId, ": Post Album ", reqObject);
await knex.transaction(async (trx) => {
try {
// Start retrieving artists.
const artistIdsPromise = reqObject.artistIds ?
trx.select('id')
.from('artists')
.where({ 'user': userId })
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('tags')
.where({ 'user': userId })
.whereIn('id', reqObject.tagIds)
.then((as: any) => as.map((a: any) => a['id'])) :
(async () => { return [] })();
// Wait for the requests to finish.
var [artists, tags] = await Promise.all([artistIdsPromise, tagIdsPromise]);;
// Check that we found all artists and tags we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length)) {
const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for CreateAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
res.status(200).send(responseObject);
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
throw e;
}
// Create the album.
const albumId = (await trx('albums')
.insert({
name: reqObject.name,
storeLinks: JSON.stringify(reqObject.storeLinks || []),
user: userId,
})
.returning('id') // Needed for Postgres
)[0];
// Link the artists via the linking table.
if (artists && artists.length) {
await trx('artists_albums').insert(
artists.map((artistId: number) => {
return {
artistId: artistId,
albumId: albumId,
}
})
)
}
})
// Link the tags via the linking table.
if (tags && tags.length) {
await trx('albums_tags').insert(
tags.map((tagId: number) => {
return {
albumId: albumId,
tagId: tagId,
}
})
)
}
// Respond to the request.
const responseObject: api.CreateSongResponse = {
id: albumId
};
res.status(200).send(responseObject);
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
}
export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifyAlbumRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid PutAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.ModifyAlbumRequest = req.body;
const { id: userId } = req.user;
console.log("User ", userId, ": Put Album ", reqObject);
await knex.transaction(async (trx) => {
try {
// Start retrieving the album itself.
const albumPromise = trx.select('id')
.from('albums')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
// Start retrieving artists.
const artistIdsPromise = reqObject.artistIds ?
trx.select('artistId')
.from('artists_albums')
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['artistId'])) :
(async () => { return undefined })();
// Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('albums_tags')
.whereIn('id', reqObject.tagIds)
.then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return undefined })();
// Wait for the requests to finish.
var [album, artists, tags] = await Promise.all([albumPromise, artistIdsPromise, tagIdsPromise]);;
export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifyAlbumRequest(req)) {
// Check that we found all objects we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) ||
!album) {
const e: EndpointError = {
internalMessage: 'Invalid PutAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifyAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.ModifyAlbumRequest = req.body;
const { id: userId } = req.user;
console.log("User ", userId, ": Put Album ", reqObject);
await knex.transaction(async (trx) => {
try {
// Start retrieving the album itself.
const albumPromise = trx.select('id')
.from('albums')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
// Start retrieving artists.
const artistIdsPromise = reqObject.artistIds ?
trx.select('artistId')
.from('artists_albums')
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['artistId'])) :
(async () => { return undefined })();
// Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('albums_tags')
.whereIn('id', reqObject.tagIds)
.then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return undefined })();
// Wait for the requests to finish.
var [album, artists, tags] = await Promise.all([albumPromise, artistIdsPromise, tagIdsPromise]);;
// Check that we found all objects we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) ||
!album) {
const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifyAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
// Modify the album.
var update: any = {};
if ("name" in reqObject) { update["name"] = reqObject.name; }
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); }
const modifyAlbumPromise = trx('albums')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.update(update)
// Remove unlinked artists.
// TODO: test this!
const removeUnlinkedArtists = artists ? trx('artists_albums')
.where({ 'albumId': req.params.id })
.whereNotIn('artistId', reqObject.artistIds || [])
.delete() : undefined;
// Remove unlinked tags.
// TODO: test this!
const removeUnlinkedTags = tags ? trx('albums_tags')
.where({ 'albumId': req.params.id })
.whereNotIn('tagId', reqObject.tagIds || [])
.delete() : undefined;
// Link new artists.
// TODO: test this!
const addArtists = artists ? trx('artists_albums')
.where({ 'albumId': req.params.id })
.then((as: any) => as.map((a: any) => a['artistId']))
.then((doneArtistIds: number[]) => {
// Get the set of artists that are not yet linked
const toLink = artists.filter((id: number) => {
return !doneArtistIds.includes(id);
});
const insertObjects = toLink.map((artistId: number) => {
return {
artistId: artistId,
albumId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('artists_albums').insert(obj)
)
);
}) : undefined;
// Link new tags.
// TODO: test this!
const addTags = tags ? trx('albums_tags')
.where({ 'albumId': req.params.id })
.then((ts: any) => ts.map((t: any) => t['tagId']))
.then((doneTagIds: number[]) => {
// Get the set of tags that are not yet linked
const toLink = tags.filter((id: number) => {
return !doneTagIds.includes(id);
});
const insertObjects = toLink.map((tagId: number) => {
return {
tagId: tagId,
albumId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('albums_tags').insert(obj)
)
);
}) : undefined;
// Modify the album.
var update: any = {};
if ("name" in reqObject) { update["name"] = reqObject.name; }
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); }
const modifyAlbumPromise = trx('albums')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.update(update)
// Remove unlinked artists.
// TODO: test this!
const removeUnlinkedArtists = artists ? trx('artists_albums')
.where({ 'albumId': req.params.id })
.whereNotIn('artistId', reqObject.artistIds || [])
.delete() : undefined;
// Remove unlinked tags.
// TODO: test this!
const removeUnlinkedTags = tags ? trx('albums_tags')
.where({ 'albumId': req.params.id })
.whereNotIn('tagId', reqObject.tagIds || [])
.delete() : undefined;
// Link new artists.
// TODO: test this!
const addArtists = artists ? trx('artists_albums')
.where({ 'albumId': req.params.id })
.then((as: any) => as.map((a: any) => a['artistId']))
.then((doneArtistIds: number[]) => {
// Get the set of artists that are not yet linked
const toLink = artists.filter((id: number) => {
return !doneArtistIds.includes(id);
});
const insertObjects = toLink.map((artistId: number) => {
return {
artistId: artistId,
albumId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('artists_albums').insert(obj)
)
);
}) : undefined;
// Link new tags.
// TODO: test this!
const addTags = tags ? trx('albums_tags')
.where({ 'albumId': req.params.id })
.then((ts: any) => ts.map((t: any) => t['tagId']))
.then((doneTagIds: number[]) => {
// Get the set of tags that are not yet linked
const toLink = tags.filter((id: number) => {
return !doneTagIds.includes(id);
});
const insertObjects = toLink.map((tagId: number) => {
return {
tagId: tagId,
albumId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('albums_tags').insert(obj)
)
);
}) : undefined;
// Wait for all operations to finish.
await Promise.all([
modifyAlbumPromise,
removeUnlinkedArtists,
removeUnlinkedTags,
addArtists,
addTags
]);
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
// Wait for all operations to finish.
await Promise.all([
modifyAlbumPromise,
removeUnlinkedArtists,
removeUnlinkedTags,
addArtists,
addTags
]);
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
}

@ -15,10 +15,17 @@ export const GetArtist: EndpointHandler = async (req: any, res: any, knex: Knex)
const { id: userId } = req.user;
try {
const tagIds = Array.from(new Set((await knex.select('tagId')
const tags: api.TagDetailsResponseWithId[] = await knex.select('tagId')
.from('artists_tags')
.where({ 'artistId': req.params.id })
).map((tag: any) => tag['tagId'])));
.then((ts: any) => {
return Array.from(new Set(
ts.map((tag: any) => tag['tagId'])
)) as number[];
})
.then((ids: number[]) => knex.select(['id', 'name', 'parentId'])
.from('tags')
.whereIn('id', ids));
const results = await knex.select(['id', 'name', 'storeLinks'])
.from('artists')
@ -28,7 +35,7 @@ export const GetArtist: EndpointHandler = async (req: any, res: any, knex: Knex)
if (results[0]) {
const response: api.ArtistDetailsResponse = {
name: results[0].name,
tagIds: tagIds,
tags: tags,
storeLinks: asJson(results[0].storeLinks),
}
await res.send(response);

@ -120,76 +120,86 @@ export const PostSong: EndpointHandler = async (req: any, res: any, knex: Knex)
export const GetSong: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkSongDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid GetSong request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
const e: EndpointError = {
internalMessage: 'Invalid GetSong request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const { id: userId } = req.user;
try {
const tagIdsPromise: Promise<number[]> = knex.select('tagId')
.from('songs_tags')
.where({ 'songId': req.params.id })
.then((ts: any) => {
return Array.from(new Set(
ts.map((tag: any) => tag['tagId'])
));
})
const albumIdsPromise: Promise<number[]> = knex.select('albumId')
.from('songs_albums')
.where({ 'songId': req.params.id })
.then((as: any) => {
return Array.from(new Set(
as.map((album: any) => album['albumId'])
));
})
const artistIdsPromise: Promise<number[]> = knex.select('artistId')
.from('songs_artists')
.where({ 'songId': req.params.id })
.then((as: any) => {
return Array.from(new Set(
as.map((artist: any) => artist['artistId'])
));
})
const songPromise = await knex.select(['id', 'title', 'storeLinks'])
.from('songs')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.then((ss: any) => ss[0])
const [tags, albums, artists, song] =
await Promise.all([tagIdsPromise, albumIdsPromise, artistIdsPromise, songPromise]);
if (song) {
const response: api.SongDetailsResponse = {
title: song.title,
tagIds: tags,
artistIds: artists,
albumIds: albums,
storeLinks: asJson(song.storeLinks),
}
await res.send(response);
} else {
await res.status(404).send({});
const tagsPromise: Promise<api.TagDetailsResponseWithId[]> = knex.select('tagId')
.from('songs_tags')
.where({ 'songId': req.params.id })
.then((ts: any) => {
return Array.from(new Set(
ts.map((tag: any) => tag['tagId'])
)) as number[];
})
.then((ids: number[]) => knex.select(['id', 'name', 'parentId'])
.from('tags')
.whereIn('id', ids))
const albumsPromise: Promise<api.AlbumDetailsResponseWithId[]> = knex.select('albumId')
.from('songs_albums')
.where({ 'songId': req.params.id })
.then((as: any) => {
return Array.from(new Set(
as.map((album: any) => album['albumId'])
)) as number[];
})
.then((ids: number[]) => knex.select(['id', 'name', 'storeLinks'])
.from('albums')
.whereIn('id', ids))
const artistsPromise: Promise<api.ArtistDetailsResponseWithId[]> = knex.select('artistId')
.from('songs_artists')
.where({ 'songId': req.params.id })
.then((as: any) => {
return Array.from(new Set(
as.map((artist: any) => artist['artistId'])
)) as number[];
})
.then((ids: number[]) => knex.select(['id', 'name', 'storeLinks'])
.from('albums')
.whereIn('id', ids))
const songPromise = await knex.select(['id', 'title', 'storeLinks'])
.from('songs')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.then((ss: any) => ss[0])
const [tags, albums, artists, song] =
await Promise.all([tagsPromise, albumsPromise, artistsPromise, songPromise]);
if (song) {
const response: api.SongDetailsResponse = {
title: song.title,
tags: tags,
artists: artists,
albums: albums,
storeLinks: asJson(song.storeLinks),
}
await res.send(response);
} else {
await res.status(404).send({});
}
} catch (e) {
catchUnhandledErrors(e)
catchUnhandledErrors(e)
}
}
export const PutSong: EndpointHandler = async (req: any, res: any, knex: Knex) => {
if (!api.checkModifySongRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid PutSong request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
const e: EndpointError = {
internalMessage: 'Invalid PutSong request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.ModifySongRequest = req.body;
const { id: userId } = req.user;
@ -197,176 +207,176 @@ export const PutSong: EndpointHandler = async (req: any, res: any, knex: Knex) =
console.log("User ", userId, ": Put Song ", reqObject);
await knex.transaction(async (trx) => {
try {
// Retrieve the song to be modified itself.
const songPromise = trx.select('id')
.from('songs')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Start retrieving artists.
const artistIdsPromise = reqObject.artistIds ?
trx.select('artistId')
.from('songs_artists')
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['artistId'])) :
(async () => { return undefined })();
// Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('songs_tags')
.whereIn('id', reqObject.tagIds)
.then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return undefined })();
// Start retrieving albums.
const albumIdsPromise = reqObject.albumIds ?
trx.select('id')
.from('songs_albums')
.whereIn('id', reqObject.albumIds)
.then((as: any) => as.map((a: any) => a['albumId'])) :
(async () => { return undefined })();
// Wait for the requests to finish.
var [song, artists, tags, albums] =
await Promise.all([songPromise, artistIdsPromise, tagIdsPromise, albumIdsPromise]);;
// Check that we found all objects we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) ||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length) ||
!song) {
const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifySong request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
// Modify the song.
var update: any = {};
if ("title" in reqObject) { update["title"] = reqObject.title; }
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); }
const modifySongPromise = trx('songs')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.update(update)
// Remove unlinked artists.
// TODO: test this!
const removeUnlinkedArtists = artists ? trx('songs_artists')
.where({ 'songId': req.params.id })
.whereNotIn('artistId', reqObject.artistIds || [])
.delete() : undefined;
// Remove unlinked tags.
// TODO: test this!
const removeUnlinkedTags = tags ? trx('songs_tags')
.where({ 'songId': req.params.id })
.whereNotIn('tagId', reqObject.tagIds || [])
.delete() : undefined;
// Remove unlinked albums.
// TODO: test this!
const removeUnlinkedAlbums = albums ? trx('songs_albums')
.where({ 'songId': req.params.id })
.whereNotIn('albumId', reqObject.albumIds || [])
.delete() : undefined;
// Link new artists.
// TODO: test this!
const addArtists = artists ? trx('songs_artists')
.where({ 'songId': req.params.id })
.then((as: any) => as.map((a: any) => a['artistId']))
.then((doneArtistIds: number[]) => {
// Get the set of artists that are not yet linked
const toLink = artists.filter((id: number) => {
return !doneArtistIds.includes(id);
});
const insertObjects = toLink.map((artistId: number) => {
return {
artistId: artistId,
songId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('songs_artists').insert(obj)
)
);
}) : undefined;
// Link new tags.
// TODO: test this!
const addTags = tags ? trx('songs_tags')
.where({ 'songId': req.params.id })
.then((ts: any) => ts.map((t: any) => t['tagId']))
.then((doneTagIds: number[]) => {
// Get the set of tags that are not yet linked
const toLink = tags.filter((id: number) => {
return !doneTagIds.includes(id);
});
const insertObjects = toLink.map((tagId: number) => {
return {
tagId: tagId,
songId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('songs_tags').insert(obj)
)
);
}) : undefined;
// Link new albums.
// TODO: test this!
const addAlbums = albums ? trx('songs_albums')
.where({ 'albumId': req.params.id })
.then((as: any) => as.map((a: any) => a['albumId']))
.then((doneAlbumIds: number[]) => {
// Get the set of albums that are not yet linked
const toLink = albums.filter((id: number) => {
return !doneAlbumIds.includes(id);
});
const insertObjects = toLink.map((albumId: number) => {
return {
albumId: albumId,
songId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('songs_albums').insert(obj)
)
);
}) : undefined;
// Wait for all operations to finish.
await Promise.all([
modifySongPromise,
removeUnlinkedArtists,
removeUnlinkedTags,
removeUnlinkedAlbums,
addArtists,
addTags,
addAlbums,
]);
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
try {
// Retrieve the song to be modified itself.
const songPromise = trx.select('id')
.from('songs')
.where({ 'user': userId })
.where({ id: req.params.id })
.then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
// Start retrieving artists.
const artistIdsPromise = reqObject.artistIds ?
trx.select('artistId')
.from('songs_artists')
.whereIn('id', reqObject.artistIds)
.then((as: any) => as.map((a: any) => a['artistId'])) :
(async () => { return undefined })();
// Start retrieving tags.
const tagIdsPromise = reqObject.tagIds ?
trx.select('id')
.from('songs_tags')
.whereIn('id', reqObject.tagIds)
.then((ts: any) => ts.map((t: any) => t['tagId'])) :
(async () => { return undefined })();
// Start retrieving albums.
const albumIdsPromise = reqObject.albumIds ?
trx.select('id')
.from('songs_albums')
.whereIn('id', reqObject.albumIds)
.then((as: any) => as.map((a: any) => a['albumId'])) :
(async () => { return undefined })();
// Wait for the requests to finish.
var [song, artists, tags, albums] =
await Promise.all([songPromise, artistIdsPromise, tagIdsPromise, albumIdsPromise]);;
// Check that we found all objects we need.
if ((reqObject.artistIds && artists.length !== reqObject.artistIds.length) ||
(reqObject.tagIds && tags.length !== reqObject.tagIds.length) ||
(reqObject.albumIds && albums.length !== reqObject.albumIds.length) ||
!song) {
const e: EndpointError = {
internalMessage: 'Not all albums and/or artists and/or tags exist for ModifySong request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
// Modify the song.
var update: any = {};
if ("title" in reqObject) { update["title"] = reqObject.title; }
if ("storeLinks" in reqObject) { update["storeLinks"] = JSON.stringify(reqObject.storeLinks || []); }
const modifySongPromise = trx('songs')
.where({ 'user': userId })
.where({ 'id': req.params.id })
.update(update)
// Remove unlinked artists.
// TODO: test this!
const removeUnlinkedArtists = artists ? trx('songs_artists')
.where({ 'songId': req.params.id })
.whereNotIn('artistId', reqObject.artistIds || [])
.delete() : undefined;
// Remove unlinked tags.
// TODO: test this!
const removeUnlinkedTags = tags ? trx('songs_tags')
.where({ 'songId': req.params.id })
.whereNotIn('tagId', reqObject.tagIds || [])
.delete() : undefined;
// Remove unlinked albums.
// TODO: test this!
const removeUnlinkedAlbums = albums ? trx('songs_albums')
.where({ 'songId': req.params.id })
.whereNotIn('albumId', reqObject.albumIds || [])
.delete() : undefined;
// Link new artists.
// TODO: test this!
const addArtists = artists ? trx('songs_artists')
.where({ 'songId': req.params.id })
.then((as: any) => as.map((a: any) => a['artistId']))
.then((doneArtistIds: number[]) => {
// Get the set of artists that are not yet linked
const toLink = artists.filter((id: number) => {
return !doneArtistIds.includes(id);
});
const insertObjects = toLink.map((artistId: number) => {
return {
artistId: artistId,
songId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('songs_artists').insert(obj)
)
);
}) : undefined;
// Link new tags.
// TODO: test this!
const addTags = tags ? trx('songs_tags')
.where({ 'songId': req.params.id })
.then((ts: any) => ts.map((t: any) => t['tagId']))
.then((doneTagIds: number[]) => {
// Get the set of tags that are not yet linked
const toLink = tags.filter((id: number) => {
return !doneTagIds.includes(id);
});
const insertObjects = toLink.map((tagId: number) => {
return {
tagId: tagId,
songId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('songs_tags').insert(obj)
)
);
}) : undefined;
// Link new albums.
// TODO: test this!
const addAlbums = albums ? trx('songs_albums')
.where({ 'albumId': req.params.id })
.then((as: any) => as.map((a: any) => a['albumId']))
.then((doneAlbumIds: number[]) => {
// Get the set of albums that are not yet linked
const toLink = albums.filter((id: number) => {
return !doneAlbumIds.includes(id);
});
const insertObjects = toLink.map((albumId: number) => {
return {
albumId: albumId,
songId: req.params.id,
}
})
// Link them
return Promise.all(
insertObjects.map((obj: any) =>
trx('songs_albums').insert(obj)
)
);
}) : undefined;
// Wait for all operations to finish.
await Promise.all([
modifySongPromise,
removeUnlinkedArtists,
removeUnlinkedTags,
removeUnlinkedAlbums,
addArtists,
addTags,
addAlbums,
]);
// Respond to the request.
res.status(200).send();
} catch (e) {
catchUnhandledErrors(e);
trx.rollback();
}
})
}
Loading…
Cancel
Save