diff --git a/client/package-lock.json b/client/package-lock.json
index 0e5418c..c3f1ea8 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1837,6 +1837,14 @@
"hoist-non-react-statics": "^3.3.0"
}
},
+ "@types/http-proxy": {
+ "version": "1.17.4",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
+ "integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/istanbul-lib-coverage": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -6910,14 +6918,55 @@
}
},
"http-proxy-middleware": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
- "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz",
+ "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==",
"requires": {
- "http-proxy": "^1.17.0",
- "is-glob": "^4.0.0",
- "lodash": "^4.17.11",
- "micromatch": "^3.1.10"
+ "@types/http-proxy": "^1.17.4",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "lodash": "^4.17.20",
+ "micromatch": "^4.0.2"
+ },
+ "dependencies": {
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "micromatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
+ "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.0.5"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ }
}
},
"http-signature": {
@@ -14140,6 +14189,17 @@
}
}
},
+ "http-proxy-middleware": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
+ "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
+ "requires": {
+ "http-proxy": "^1.17.0",
+ "is-glob": "^4.0.0",
+ "lodash": "^4.17.11",
+ "micromatch": "^3.1.10"
+ }
+ },
"is-absolute-url": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz",
diff --git a/client/package.json b/client/package.json
index 28f7ee6..f0412e6 100644
--- a/client/package.json
+++ b/client/package.json
@@ -17,6 +17,7 @@
"@types/react-router-dom": "^5.1.5",
"@types/tiny-async-pool": "^1.0.0",
"@types/uuid": "^8.3.0",
+ "http-proxy-middleware": "^1.0.6",
"jsurl": "^0.1.5",
"lodash": "^4.17.20",
"material-table": "^1.69.0",
@@ -34,7 +35,7 @@
"uuid": "^8.3.0"
},
"scripts": {
- "dev": "BROWSER=none react-scripts start",
+ "dev": "REACT_APP_BACKEND='/api' BROWSER=none react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
@@ -53,6 +54,5 @@
"last 1 firefox version",
"last 1 safari version"
]
- },
- "proxy": "http://localhost:5000/"
+ }
}
diff --git a/client/src/api/api.ts b/client/src/api/api.ts
index 7cc54af..2862622 100644
--- a/client/src/api/api.ts
+++ b/client/src/api/api.ts
@@ -9,5 +9,6 @@
export * from './types/resources';
export * from './endpoints/auth';
+export * from './endpoints/importexport';
export * from './endpoints/resources';
export * from './endpoints/query';
\ No newline at end of file
diff --git a/client/src/api/endpoints/importexport.ts b/client/src/api/endpoints/importexport.ts
new file mode 100644
index 0000000..d7432ee
--- /dev/null
+++ b/client/src/api/endpoints/importexport.ts
@@ -0,0 +1,44 @@
+
+// This interface describes a JSON format in which the "interesting part"
+// of the entire database for a user can be imported/exported.
+// Worth noting is that the IDs used in this format only exist for cross-
+// referencing between objects. They do not correspond to IDs in the actual
+// database.
+// Upon import, they might be replaced, and upon export, they might be randomly
+
+import { AlbumWithRefsWithId, ArtistWithRefsWithId, isAlbumWithRefs, isArtistWithRefs, isTagWithRefs, isTrackBaseWithRefs, isTrackWithRefs, TagWithRefsWithId, TrackWithRefsWithId } from "../types/resources";
+
+// generated.
+export interface DBImportExportFormat {
+ tracks: TrackWithRefsWithId[],
+ albums: AlbumWithRefsWithId[],
+ artists: ArtistWithRefsWithId[],
+ tags: TagWithRefsWithId[],
+}
+
+// Get a full export of a user's database (GET).
+export const DBExportEndpoint = "/export";
+export type DBExportResponse = DBImportExportFormat;
+
+// Fully replace the user's database by an import (POST).
+export const DBImportEndpoint = "/import";
+export type DBImportRequest = DBImportExportFormat;
+export type DBImportResponse = void;
+export const checkDBImportRequest: (v: any) => boolean = (v: any) => {
+ return 'tracks' in v &&
+ 'albums' in v &&
+ 'artists' in v &&
+ 'tags' in v &&
+ v.tracks.reduce((prev: boolean, cur: any) => {
+ return prev && isTrackWithRefs(cur);
+ }, true) &&
+ v.albums.reduce((prev: boolean, cur: any) => {
+ return prev && isAlbumWithRefs(cur);
+ }, true) &&
+ v.artists.reduce((prev: boolean, cur: any) => {
+ return prev && isArtistWithRefs(cur);
+ }, true) &&
+ v.tags.reduce((prev: boolean, cur: any) => {
+ return prev && isTagWithRefs(cur);
+ }, true);
+}
diff --git a/client/src/api/types/resources.ts b/client/src/api/types/resources.ts
index fdadd35..99eb4f4 100644
--- a/client/src/api/types/resources.ts
+++ b/client/src/api/types/resources.ts
@@ -49,7 +49,7 @@ export function isTrackBase(q: any): q is TrackBase {
return q.mbApi_typename && q.mbApi_typename === "track";
}
export function isTrackBaseWithRefs(q: any): q is TrackBaseWithRefs {
- return isTrackBase(q) && "artistIds" in q && "tagIds" in q && "albumId" in q;
+ return isTrackBase(q);
}
export function isTrackWithRefs(q: any): q is TrackWithRefs {
return isTrackBaseWithRefs(q) && "name" in q;
@@ -65,10 +65,12 @@ export interface ArtistBase {
export interface ArtistBaseWithRefs extends ArtistBase {
albumIds?: number[],
tagIds?: number[],
+ trackIds?: number[],
}
export interface ArtistBaseWithDetails extends ArtistBase {
albums: AlbumWithId[],
tags: TagWithId[],
+ tracks: TrackWithId[],
}
export interface ArtistWithDetails extends ArtistBaseWithDetails {
name: string,
@@ -77,6 +79,7 @@ export interface ArtistWithRefs extends ArtistBaseWithRefs {
name: string,
albumIds: number[],
tagIds: number[],
+ trackIds: number[],
}
export interface Artist extends ArtistBase {
name: string,
@@ -94,10 +97,10 @@ export function isArtistBase(q: any): q is ArtistBase {
return q.mbApi_typename && q.mbApi_typename === "artist";
}
export function isArtistBaseWithRefs(q: any): q is ArtistBaseWithRefs {
- return isArtistBase(q) && q && "tagIds" in q && "albumIds" in q;
+ return isArtistBase(q);
}
export function isArtistWithRefs(q: any): q is ArtistWithRefs {
- return isTrackBaseWithRefs(q) && "name" in q;
+ return isArtistBaseWithRefs(q) && "name" in q;
}
@@ -142,7 +145,7 @@ export function isAlbumBase(q: any): q is AlbumBase {
return q.mbApi_typename && q.mbApi_typename === "album";
}
export function isAlbumBaseWithRefs(q: any): q is AlbumBaseWithRefs {
- return isAlbumBase(q) && "artistIds" in q && "trackIds" in q && "tagIds" in q;
+ return isAlbumBase(q);
}
export function isAlbumWithRefs(q: any): q is AlbumWithRefs {
return isAlbumBaseWithRefs(q) && "name" in q;
@@ -183,9 +186,10 @@ export function isTagBase(q: any): q is TagBase {
return q.mbApi_typename && q.mbApi_typename === "tag";
}
export function isTagBaseWithRefs(q: any): q is TagBaseWithRefs {
- return isTagBase(q) && "parentId" in q;
+ return isTagBase(q);
}
export function isTagWithRefs(q: any): q is TagWithRefs {
+ console.log("Check", q)
return isTagBaseWithRefs(q) && "name" in q;
}
diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx
index 8b82c98..9270c6b 100644
--- a/client/src/components/MainWindow.tsx
+++ b/client/src/components/MainWindow.tsx
@@ -17,6 +17,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import { ProvideIntegrations } from '../lib/integration/useIntegrations';
import ManageLinksWindow from './windows/manage_links/ManageLinksWindow';
import ManageWindow, { ManageWhat } from './windows/manage/ManageWindow';
+import TrackWindow from './windows/track/TrackWindow';
const darkTheme = createMuiTheme({
palette: {
@@ -84,9 +85,9 @@ export default function MainWindow(props: any) {
-
+
-
+
@@ -96,6 +97,10 @@ export default function MainWindow(props: any) {
+
+
+
+
diff --git a/client/src/components/tables/ResultsTable.tsx b/client/src/components/tables/ResultsTable.tsx
index f581bdf..4f605d3 100644
--- a/client/src/components/tables/ResultsTable.tsx
+++ b/client/src/components/tables/ResultsTable.tsx
@@ -4,12 +4,12 @@ import stringifyList from '../../lib/stringifyList';
import { useHistory } from 'react-router';
export interface TrackGetters {
- getTitle: (track: any) => string,
+ getName: (track: any) => string,
getId: (track: any) => number,
getArtistNames: (track: any) => string[],
getArtistIds: (track: any) => number[],
- getAlbumNames: (track: any) => string[],
- getAlbumIds: (track: any) => number[],
+ getAlbumName: (track: any) => string | undefined,
+ getAlbumId: (track: any) => number | undefined,
getTagNames: (track: any) => string[][], // Each tag is represented as a series of strings.
getTagIds: (track: any) => number[][], // Each tag is represented as a series of ids.
}
@@ -45,14 +45,13 @@ export default function TrackTable(props: {
{props.tracks.map((track: any) => {
- const title = props.trackGetters.getTitle(track);
+ const name = props.trackGetters.getName(track);
// TODO: display artists and albums separately!
const artistNames = props.trackGetters.getArtistNames(track);
const artist = stringifyList(artistNames);
const mainArtistId = props.trackGetters.getArtistIds(track)[0];
- const albumNames = props.trackGetters.getAlbumNames(track);
- const album = stringifyList(albumNames);
- const mainAlbumId = props.trackGetters.getAlbumIds(track)[0];
+ const album = props.trackGetters.getAlbumName(track);
+ const albumId = props.trackGetters.getAlbumId(track);
const trackId = props.trackGetters.getId(track);
const tagIds = props.trackGetters.getTagIds(track);
@@ -61,7 +60,7 @@ export default function TrackTable(props: {
}
const onClickAlbum = () => {
- history.push('/album/' + mainAlbumId);
+ history.push('/album/' + albumId || '');
}
const onClickTrack = () => {
@@ -99,10 +98,10 @@ export default function TrackTable(props: {
;
}
- return
- {title}
+ return
+ {name}
{artist}
- {album}
+ {album ? {album} : }
{tags}
diff --git a/client/src/components/windows/manage/ManageWindow.tsx b/client/src/components/windows/manage/ManageWindow.tsx
index 0928001..edf83e4 100644
--- a/client/src/components/windows/manage/ManageWindow.tsx
+++ b/client/src/components/windows/manage/ManageWindow.tsx
@@ -7,12 +7,15 @@ import Alert from '@material-ui/lab/Alert';
import { Link } from 'react-router-dom';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
+import SaveIcon from '@material-ui/icons/Save';
import ManageLinksWindow from '../manage_links/ManageLinksWindow';
import ManageTagsWindow from '../manage_tags/ManageTagsWindow';
+import ManageImportExportWindow from '../manage_importexport/ManageImportExport';
export enum ManageWhat {
Tags = 0,
Links,
+ ImportExport,
}
export default function ManageWindow(props: {
@@ -52,8 +55,15 @@ export default function ManageWindow(props: {
selected={props.selectedWindow === ManageWhat.Links}
onClick={() => history.push('/manage/links')}
/>
+ }
+ selected={props.selectedWindow === ManageWhat.ImportExport}
+ onClick={() => history.push('/manage/importexport')}
+ />
{props.selectedWindow === ManageWhat.Tags && }
{props.selectedWindow === ManageWhat.Links && }
+ {props.selectedWindow === ManageWhat.ImportExport && }
}
\ No newline at end of file
diff --git a/client/src/components/windows/manage_importexport/ManageImportExport.tsx b/client/src/components/windows/manage_importexport/ManageImportExport.tsx
new file mode 100644
index 0000000..e2d5cdf
--- /dev/null
+++ b/client/src/components/windows/manage_importexport/ManageImportExport.tsx
@@ -0,0 +1,65 @@
+import React, { useReducer, useState } from 'react';
+import { WindowState } from "../Windows";
+import { Box, Paper, Typography, TextField, Button } from "@material-ui/core";
+import SaveIcon from '@material-ui/icons/Save';
+import { Alert } from '@material-ui/lab';
+import * as serverApi from '../../../api/api';
+
+export interface ManageImportExportWindowState extends WindowState {
+ dummy: boolean
+}
+export enum ManageImportExportWindowActions {
+ SetDummy = "SetDummy",
+}
+export function ManageImportExportWindowReducer(state: ManageImportExportWindowState, action: any) {
+ switch (action.type) {
+ case ManageImportExportWindowActions.SetDummy: {
+ return state;
+ }
+ default:
+ throw new Error("Unimplemented ManageImportExportWindow state update.")
+ }
+}
+
+export default function ManageImportExportWindow(props: {}) {
+ const [state, dispatch] = useReducer(ManageImportExportWindowReducer, {
+ dummy: true,
+ });
+
+ return
+}
+
+export function ManageImportExportWindowControlled(props: {
+ state: ManageImportExportWindowState,
+ dispatch: (action: any) => void,
+}) {
+ return <>
+
+
+
+
+
+ Import / Export
+
+
+ An exported database contains all your artists, albums, tracks and tags.
+ It is represented as a JSON structure.
+
+
+ Upon importing a previously exported database, your database will be completely replaced!
+
+
+
+
+
+
+ >
+}
\ No newline at end of file
diff --git a/client/src/components/windows/manage_links/BatchLinkDialog.tsx b/client/src/components/windows/manage_links/BatchLinkDialog.tsx
index ba9e8dd..0184e90 100644
--- a/client/src/components/windows/manage_links/BatchLinkDialog.tsx
+++ b/client/src/components/windows/manage_links/BatchLinkDialog.tsx
@@ -171,7 +171,7 @@ async function doLinking(
[ResourceType.Artist]: getArtist,
}
let queryFuncs: any = {
- [ResourceType.Track]: (s: any) => `${s.title}` +
+ [ResourceType.Track]: (s: any) => `${s.name}` +
`${s.artists && s.artists.length > 0 && ` ${s.artists[0].name}` || ''}` +
`${s.albums && s.albums.length > 0 && ` ${s.albums[0].name}` || ''}`,
[ResourceType.Album]: (s: any) => `${s.name}` +
diff --git a/client/src/components/windows/query/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx
index 779fa78..7f3db2e 100644
--- a/client/src/components/windows/query/QueryWindow.tsx
+++ b/client/src/components/windows/query/QueryWindow.tsx
@@ -62,7 +62,7 @@ async function getTrackNames(filter: string) {
0, -1, QueryResponseType.Details
);
- return [...(new Set([...(tracks.map((s: any) => s.title))]))];
+ return [...(new Set([...(tracks.map((s: any) => s.name))]))];
}
async function getTagItems(): Promise {
diff --git a/client/src/lib/trackGetters.tsx b/client/src/lib/trackGetters.tsx
index 6582f14..1c4a709 100644
--- a/client/src/lib/trackGetters.tsx
+++ b/client/src/lib/trackGetters.tsx
@@ -1,10 +1,10 @@
export const trackGetters = {
- getTitle: (track: any) => track.title,
+ getName: (track: any) => track.name,
getId: (track: any) => track.trackId,
getArtistNames: (track: any) => track.artists.map((a: any) => a.name),
getArtistIds: (track: any) => track.artists.map((a: any) => a.artistId),
- getAlbumNames: (track: any) => track.albums.map((a: any) => a.name),
- getAlbumIds: (track: any) => track.albums.map((a: any) => a.albumId),
+ getAlbumName: (track: any) => track.album ? track.album.name : undefined,
+ getAlbumId: (track: any) => track.album ? track.album.albumId : undefined,
getTagNames: (track: any) => {
// Recursively resolve the name.
const resolveTag = (tag: any) => {
diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js
new file mode 100644
index 0000000..1e39e75
--- /dev/null
+++ b/client/src/setupProxy.js
@@ -0,0 +1,11 @@
+const { createProxyMiddleware } = require('http-proxy-middleware');
+
+module.exports = function(app) {
+ app.use(
+ process.env.REACT_APP_BACKEND,
+ createProxyMiddleware({
+ target: 'http://localhost:5000',
+ changeOrigin: true,
+ })
+ );
+};
\ No newline at end of file
diff --git a/scripts/gpm_retrieve/gpm_retrieve.py b/scripts/gpm_retrieve/gpm_retrieve.py
index 7efa4e9..97828d3 100755
--- a/scripts/gpm_retrieve/gpm_retrieve.py
+++ b/scripts/gpm_retrieve/gpm_retrieve.py
@@ -13,7 +13,7 @@ creds_path = sys.path[0] + '/mobileclient.cred'
def authenticate(api):
creds = api.perform_oauth(storage_filepath=creds_path, open_browser=False)
-def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, songs):
+def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, tracks):
# First, attempt to login and start a session.
s = requests.Session()
response = s.post(mudbase_api + '/login?username='
@@ -25,26 +25,30 @@ def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, songs):
print("Unable to log in to MuDBase API.")
# Helpers
- def getArtistStoreIds(song):
- if 'artistId' in song:
- return [song['artistId'][0]]
+ def getArtistStoreIds(track):
+ if 'artistId' in track:
+ return [track['artistId'][0]]
return []
- def getSongStoreIds(song):
- if 'storeId' in song:
- return [song['storeId']]
+ def getTrackStoreIds(track):
+ if 'storeId' in track:
+ return [track['storeId']]
return []
# Create GPM import tag
- gpmTagIdResponse = s.post(mudbase_api + '/tag', data={
- 'name': 'GPM Import'
+ gpmTagIdResponse = s.post(mudbase_api + '/tag', json={
+ 'name': 'GPM Import',
+ 'mbApi_typename': 'tag',
+ 'parentId': None,
}).json()
gpmTagId = gpmTagIdResponse['id']
print(f"Created tag \"GPM Import\", response: {gpmTagIdResponse}")
# Create the root genre tag
- genreRootResponse = s.post(mudbase_api + '/tag', data={
- 'name': 'Genre'
+ genreRootResponse = s.post(mudbase_api + '/tag', json={
+ 'name': 'Genre',
+ 'mbApi_typename': 'tag',
+ 'parentId': None,
}).json()
genreRootTagId = genreRootResponse['id']
print(f"Created tag \"Genre\", response: {genreRootResponse}")
@@ -54,27 +58,16 @@ def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, songs):
storedAlbums = dict()
storedGenreTags = dict()
- for song in songs:
+ for track in tracks:
# TODO: check if these items already exist
# Determine artist properties.
artist = {
- 'name': song['artist'],
- 'storeLinks': ['https://play.google.com/music/m' + id for id in getArtistStoreIds(song)],
+ 'mbApi_typename': 'artist',
+ 'name': track['artist'],
+ 'storeLinks': ['https://play.google.com/music/m' + id for id in getArtistStoreIds(track)],
'tagIds': [gpmTagId]
- } if 'artist' in song else None
-
- # Determine album properties.
- album = {
- 'name': song['album'],
- 'tagIds': [gpmTagId]
- } if 'album' in song else None
-
- # Determine genre properties.
- genre = {
- 'name': song['genre'],
- 'parentId': genreRootTagId
- } if 'genre' in song else None
+ } if 'artist' in track else None
# Upload artist if not already done
artistId = None
@@ -90,6 +83,21 @@ def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, songs):
f"Created artist \"{artist['name']}\", response: {response}")
storedArtists[artistId] = artist
+ # Determine album properties.
+ album = {
+ 'mbApi_typename': 'album',
+ 'name': track['album'],
+ 'tagIds': [gpmTagId],
+ 'artistIds': [artistId],
+ } if 'album' in track else None
+
+ # Determine genre properties.
+ genre = {
+ 'mbApi_typename': 'tag',
+ 'name': track['genre'],
+ 'parentId': genreRootTagId
+ } if 'genre' in track else None
+
# Upload album if not already done
albumId = None
if album:
@@ -118,44 +126,45 @@ def uploadLibrary(mudbase_api, mudbase_user, mudbase_password, songs):
f"Created genre tag \"Genre / {genre['name']}\", response: {response}")
storedGenreTags[genreTagId] = genre
- # Upload the song itself
+ # Upload the track itself
tagIds = [gpmTagId]
if genreTagId:
tagIds.append(genreTagId)
- _song = {
- 'title': song['title'],
+ _track = {
+ 'mbApi_typename': 'track',
+ 'name': track['title'],
'artistIds': [artistId] if artistId != None else [],
- 'albumIds': [albumId] if albumId != None else [],
+ 'albumId': albumId if albumId else None,
'tagIds': tagIds,
- 'storeLinks': ['https://play.google.com/music/m/' + id for id in getSongStoreIds(song)],
+ 'storeLinks': ['https://play.google.com/music/m/' + id for id in getTrackStoreIds(track)],
}
- response = s.post(mudbase_api + '/song', json=_song).json()
+ response = s.post(mudbase_api + '/track', json=_track).json()
print(
- f"Created song \"{song['title']}\" with artist ID {artistId}, album ID {albumId}, response: {response}")
+ f"Created track \"{track['title']}\" with artist ID {artistId}, album ID {albumId}, response: {response}")
def getData(api):
return {
- "songs": api.get_all_songs(),
+ "tracks": api.get_all_tracks(),
"playlists": api.get_all_user_playlist_contents()
}
-def getSongs(data):
- # Get songs from library
- songs = [] # data['songs']
+def getTracks(data):
+ # Get tracks from library
+ tracks = [] # data['tracks']
- # Append songs from playlists
+ # Append tracks from playlists
for playlist in data['playlists']:
for track in playlist['tracks']:
if 'track' in track:
- songs.append(track['track'])
+ tracks.append(track['track'])
- # Uniquify by using a dict. After all, same song may appear in
+ # Uniquify by using a dict. After all, same track may appear in
# multiple playlists.
- def sI(song): return song['artist'] + '-' + \
- song['title'] if 'artist' in song and 'title' in song else 'z'
- return list(dict((sI(song), song) for song in songs).values())
+ def sI(track): return track['artist'] + '-' + \
+ track['title'] if 'artist' in track and 'title' in track else 'z'
+ return list(dict((sI(track), track) for track in tracks).values())
api = Mobileclient()
@@ -182,7 +191,7 @@ if args.authenticate:
data = None
-# Determine whether we need to log in to GPM and get songs
+# Determine whether we need to log in to GPM and get tracks
if args.store_to or (not args.load_from and args.mudbase_api):
api.oauth_login(Mobileclient.FROM_MAC_ADDRESS,
oauth_credentials=creds_path)
@@ -198,11 +207,11 @@ if args.load_from:
with open(args.load_from, 'r') as f:
data = json.load(f)
-songs = getSongs(data)
-print(f"Found {len(songs)} songs.")
+tracks = getTracks(data)
+print(f"Found {len(tracks)} tracks.")
if args.mudbase_api:
api.oauth_login(Mobileclient.FROM_MAC_ADDRESS,
oauth_credentials=creds_path)
uploadLibrary(args.mudbase_api, args.mudbase_user,
- args.mudbase_password, songs)
+ args.mudbase_password, tracks)
diff --git a/server/app.ts b/server/app.ts
index 0e4bba6..ddc3d2a 100644
--- a/server/app.ts
+++ b/server/app.ts
@@ -2,6 +2,7 @@ const bodyParser = require('body-parser');
import * as api from '../client/src/api/api';
import Knex from 'knex';
+import { importExportEndpoints } from './endpoints/ImportExport';
import { queryEndpoints } from './endpoints/Query';
import { artistEndpoints } from './endpoints/Artist';
import { albumEndpoints } from './endpoints/Album';
@@ -120,6 +121,7 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
integrationEndpoints,
userEndpoints,
queryEndpoints,
+ importExportEndpoints,
].forEach((endpoints: [string, string, boolean, endpointTypes.EndpointHandler][]) => {
endpoints.forEach((endpoint: [string, string, boolean, endpointTypes.EndpointHandler]) => {
let [url, method, authenticated, handler] = endpoint;
diff --git a/server/db/Album.ts b/server/db/Album.ts
index 7c1cd69..358f7b0 100644
--- a/server/db/Album.ts
+++ b/server/db/Album.ts
@@ -3,6 +3,7 @@ import { AlbumBaseWithRefs, AlbumWithDetails, AlbumWithRefs } from "../../client
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
+var _ = require('lodash');
// Returns an album with details, or null if not found.
export async function getAlbum(id: number, userId: number, knex: Knex):
@@ -75,272 +76,260 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
// Returns the id of the created album.
export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Knex): Promise {
return await knex.transaction(async (trx) => {
- try {
- // Start retrieving artists.
- const artistIdsPromise: Promise =
- trx.select('id')
- .from('artists')
- .where({ 'user': userId })
- .whereIn('id', album.artistIds)
- .then((as: any) => as.map((a: any) => a['id']));
+ // Start retrieving artists.
+ const artistIdsPromise: Promise =
+ trx.select('id')
+ .from('artists')
+ .where({ 'user': userId })
+ .whereIn('id', album.artistIds || [])
+ .then((as: any) => as.map((a: any) => a['id']));
- // Start retrieving tags.
- const tagIdsPromise: Promise =
- trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .whereIn('id', album.tagIds)
- .then((as: any) => as.map((a: any) => a['id']));
+ // Start retrieving tags.
+ const tagIdsPromise: Promise =
+ trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .whereIn('id', album.tagIds || [])
+ .then((as: any) => as.map((a: any) => a['id']));
- // Start retrieving tracks.
- const trackIdsPromise: Promise =
- trx.select('id')
- .from('tracks')
- .where({ 'user': userId })
- .whereIn('id', album.trackIds)
- .then((as: any) => as.map((a: any) => a['id']));
-
- // Wait for the requests to finish.
- var [artists, tags, tracks] = await Promise.all([artistIdsPromise, tagIdsPromise, trackIdsPromise]);;
-
- // Check that we found all artists and tags we need.
- if ((new Set(artists.map((a: any) => a['id'])) !== new Set(album.artistIds)) ||
- (new Set(tags.map((a: any) => a['id'])) !== new Set(album.tagIds)) ||
- (new Set(tracks.map((a: any) => a['id'])) !== new Set(album.trackIds))) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Create the album.
- const albumId = (await trx('albums')
- .insert({
- name: album.name,
- storeLinks: JSON.stringify(album.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,
- }
- })
- )
- }
-
- // Link the tracks via the linking table.
- if (tracks && tracks.length) {
- await trx('tracks_albums').insert(
- tracks.map((trackId: number) => {
- return {
- albumId: albumId,
- trackId: trackId,
- }
- })
- )
- }
-
- return albumId;
-
- } catch (e) {
- trx.rollback();
+ // Start retrieving tracks.
+ const trackIdsPromise: Promise =
+ trx.select('id')
+ .from('tracks')
+ .where({ 'user': userId })
+ .whereIn('id', album.trackIds || [])
+ .then((as: any) => as.map((a: any) => a['id']));
+
+ // Wait for the requests to finish.
+ var [artists, tags, tracks] = await Promise.all([artistIdsPromise, tagIdsPromise, trackIdsPromise]);;
+
+ // Check that we found all artists and tags we need.
+ if ((!_.isEqual(artists.sort(), (album.artistIds || []).sort())) ||
+ (!_.isEqual(tags.sort(), (album.tagIds || []).sort())) ||
+ (!_.isEqual(tracks.sort(), (album.trackIds || []).sort()))) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
+ };
throw e;
}
+
+ // Create the album.
+ const albumId = (await trx('albums')
+ .insert({
+ name: album.name,
+ storeLinks: JSON.stringify(album.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,
+ }
+ })
+ )
+ }
+
+ // Link the tracks via the linking table.
+ if (tracks && tracks.length) {
+ await trx('tracks_albums').insert(
+ tracks.map((trackId: number) => {
+ return {
+ albumId: albumId,
+ trackId: trackId,
+ }
+ })
+ )
+ }
+
+ return albumId;
})
}
export async function modifyAlbum(userId: number, albumId: number, album: AlbumBaseWithRefs, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start retrieving the album itself.
- const albumIdPromise: Promise =
- trx.select('id')
- .from('albums')
- .where({ 'user': userId })
- .where({ id: albumId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
-
- // Start retrieving artists if we are modifying those.
- const artistIdsPromise: Promise =
- album.artistIds ?
- trx.select('artistId')
- .from('artists_albums')
- .whereIn('artistId', album.artistIds)
- .then((as: any) => as.map((a: any) => a['artistId']))
- : (async () => undefined)();
-
- // Start retrieving tracks if we are modifying those.
- const trackIdsPromise: Promise =
- album.trackIds ?
- trx.select('artistId')
- .from('tracks_albums')
- .whereIn('albumId', album.trackIds)
- .then((as: any) => as.map((a: any) => a['trackId']))
- : (async () => undefined)();
-
- // Start retrieving tags if we are modifying those.
- const tagIdsPromise =
- album.tagIds ?
- trx.select('id')
- .from('albums_tags')
- .whereIn('tagId', album.tagIds)
- .then((ts: any) => ts.map((t: any) => t['tagId'])) :
- (async () => undefined)();
-
- // Wait for the requests to finish.
- var [oldAlbum, artists, tags, tracks] = await Promise.all([albumIdPromise, artistIdsPromise, tagIdsPromise, trackIdsPromise]);;
-
- // Check that we found all objects we need.
- if ((!artists || new Set(artists.map((a: any) => a['id'])) !== new Set(album.artistIds)) ||
- (!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(album.tagIds)) ||
- (!tracks || new Set(tracks.map((a: any) => a['id'])) !== new Set(album.trackIds)) ||
- !oldAlbum) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Modify the album.
- var update: any = {};
- if ("name" in album) { update["name"] = album.name; }
- if ("storeLinks" in album) { update["storeLinks"] = JSON.stringify(album.storeLinks || []); }
-
- const modifyAlbumPromise = trx('albums')
+ // Start retrieving the album itself.
+ const albumIdPromise: Promise =
+ trx.select('id')
+ .from('albums')
.where({ 'user': userId })
- .where({ 'id': albumId })
- .update(update)
-
- // Remove unlinked artists.
- const removeUnlinkedArtists = artists ? trx('artists_albums')
- .where({ 'albumId': albumId })
- .whereNotIn('artistId', album.artistIds || [])
- .delete() : undefined;
-
- // Remove unlinked tags.
- const removeUnlinkedTags = tags ? trx('albums_tags')
- .where({ 'albumId': albumId })
- .whereNotIn('tagId', album.tagIds || [])
- .delete() : undefined;
-
- // Remove unlinked tracks.
- const removeUnlinkedTracks = tracks ? trx('tracks_albums')
- .where({ 'albumId': albumId })
- .whereNotIn('trackId', album.trackIds || [])
- .delete() : undefined;
-
- // Link new artists.
- const addArtists = artists ? trx('artists_albums')
- .where({ 'albumId': albumId })
- .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: albumId,
- }
- })
-
- // Link them
- return Promise.all(
- insertObjects.map((obj: any) =>
- trx('artists_albums').insert(obj)
- )
- );
- }) : undefined;
-
- // Link new tracks.
- const addTracks = tracks ? trx('tracks_albums')
- .where({ 'albumId': albumId })
- .then((as: any) => as.map((a: any) => a['trackId']))
- .then((doneTrackIds: number[]) => {
- // Get the set of artists that are not yet linked
- const toLink = (tracks || []).filter((id: number) => {
- return !doneTrackIds.includes(id);
- });
- const insertObjects = toLink.map((trackId: number) => {
- return {
- trackId: trackId,
- albumId: albumId,
- }
- })
-
- // Link them
- return Promise.all(
- insertObjects.map((obj: any) =>
- trx('tracks_albums').insert(obj)
- )
- );
- }) : undefined;
-
- // Link new tags.
- const addTags = tags ? trx('albums_tags')
- .where({ 'albumId': albumId })
- .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: albumId,
- }
- })
-
- // 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,
- removeUnlinkedTracks,
- addArtists,
- addTags,
- addTracks,
- ]);
-
- return;
-
- } catch (e) {
- trx.rollback();
+ .where({ id: albumId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
+
+ // Start retrieving artists if we are modifying those.
+ const artistIdsPromise: Promise =
+ album.artistIds ?
+ trx.select('artistId')
+ .from('artists_albums')
+ .whereIn('artistId', album.artistIds)
+ .then((as: any) => as.map((a: any) => a['artistId']))
+ : (async () => undefined)();
+
+ // Start retrieving tracks if we are modifying those.
+ const trackIdsPromise: Promise =
+ album.trackIds ?
+ trx.select('artistId')
+ .from('tracks_albums')
+ .whereIn('albumId', album.trackIds)
+ .then((as: any) => as.map((a: any) => a['trackId']))
+ : (async () => undefined)();
+
+ // Start retrieving tags if we are modifying those.
+ const tagIdsPromise =
+ album.tagIds ?
+ trx.select('id')
+ .from('albums_tags')
+ .whereIn('tagId', album.tagIds)
+ .then((ts: any) => ts.map((t: any) => t['tagId'])) :
+ (async () => undefined)();
+
+ // Wait for the requests to finish.
+ var [oldAlbum, artists, tags, tracks] = await Promise.all([albumIdPromise, artistIdsPromise, tagIdsPromise, trackIdsPromise]);;
+
+ // Check that we found all objects we need.
+ if ((!artists || !_.isEqual(artists.sort(), (album.artistIds || []).sort())) ||
+ (!tags || !_.isEqual(tags.sort(), (album.tagIds || []).sort())) ||
+ (!tracks || !_.isEqual(tracks.sort(), (album.trackIds || []).sort())) ||
+ !oldAlbum) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
+ };
throw e;
}
+
+ // Modify the album.
+ var update: any = {};
+ if ("name" in album) { update["name"] = album.name; }
+ if ("storeLinks" in album) { update["storeLinks"] = JSON.stringify(album.storeLinks || []); }
+
+ const modifyAlbumPromise = trx('albums')
+ .where({ 'user': userId })
+ .where({ 'id': albumId })
+ .update(update)
+
+ // Remove unlinked artists.
+ const removeUnlinkedArtists = artists ? trx('artists_albums')
+ .where({ 'albumId': albumId })
+ .whereNotIn('artistId', album.artistIds || [])
+ .delete() : undefined;
+
+ // Remove unlinked tags.
+ const removeUnlinkedTags = tags ? trx('albums_tags')
+ .where({ 'albumId': albumId })
+ .whereNotIn('tagId', album.tagIds || [])
+ .delete() : undefined;
+
+ // Remove unlinked tracks.
+ const removeUnlinkedTracks = tracks ? trx('tracks_albums')
+ .where({ 'albumId': albumId })
+ .whereNotIn('trackId', album.trackIds || [])
+ .delete() : undefined;
+
+ // Link new artists.
+ const addArtists = artists ? trx('artists_albums')
+ .where({ 'albumId': albumId })
+ .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: albumId,
+ }
+ })
+
+ // Link them
+ return Promise.all(
+ insertObjects.map((obj: any) =>
+ trx('artists_albums').insert(obj)
+ )
+ );
+ }) : undefined;
+
+ // Link new tracks.
+ const addTracks = tracks ? trx('tracks_albums')
+ .where({ 'albumId': albumId })
+ .then((as: any) => as.map((a: any) => a['trackId']))
+ .then((doneTrackIds: number[]) => {
+ // Get the set of artists that are not yet linked
+ const toLink = (tracks || []).filter((id: number) => {
+ return !doneTrackIds.includes(id);
+ });
+ const insertObjects = toLink.map((trackId: number) => {
+ return {
+ trackId: trackId,
+ albumId: albumId,
+ }
+ })
+
+ // Link them
+ return Promise.all(
+ insertObjects.map((obj: any) =>
+ trx('tracks_albums').insert(obj)
+ )
+ );
+ }) : undefined;
+
+ // Link new tags.
+ const addTags = tags ? trx('albums_tags')
+ .where({ 'albumId': albumId })
+ .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: albumId,
+ }
+ })
+
+ // 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,
+ removeUnlinkedTracks,
+ addArtists,
+ addTags,
+ addTracks,
+ ]);
+
+ return;
})
const e: DBError = {
@@ -353,53 +342,49 @@ export async function modifyAlbum(userId: number, albumId: number, album: AlbumB
export async function deleteAlbum(userId: number, albumId: number, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start by retrieving the album itself for sanity.
- const confirmAlbumId: number | undefined =
- await trx.select('id')
- .from('albums')
- .where({ 'user': userId })
- .where({ id: albumId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
-
- if (!confirmAlbumId) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all resources were found.',
- };
- throw e;
- }
-
- // Start deleting artist associations with the album.
- const deleteArtistsPromise: Promise =
- trx.delete()
- .from('artists_albums')
- .where({ 'albumId': albumId });
- // Start deleting tag associations with the album.
- const deleteTagsPromise: Promise =
- trx.delete()
- .from('albums_tags')
- .where({ 'albumId': albumId });
-
- // Start deleting track associations with the album.
- const deleteTracksPromise: Promise =
- trx.delete()
- .from('tracks_albums')
- .where({ 'albumId': albumId });
-
- // Start deleting the album.
- const deleteAlbumPromise: Promise =
- trx.delete()
- .from('albums')
- .where({ id: albumId });
-
- // Wait for the requests to finish.
- await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTracksPromise, deleteAlbumPromise]);
- } catch (e) {
- trx.rollback();
+ // Start by retrieving the album itself for sanity.
+ const confirmAlbumId: number | undefined =
+ await trx.select('id')
+ .from('albums')
+ .where({ 'user': userId })
+ .where({ id: albumId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
+
+ if (!confirmAlbumId) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all resources were found.',
+ };
throw e;
}
+
+ // Start deleting artist associations with the album.
+ const deleteArtistsPromise: Promise =
+ trx.delete()
+ .from('artists_albums')
+ .where({ 'albumId': albumId });
+
+ // Start deleting tag associations with the album.
+ const deleteTagsPromise: Promise =
+ trx.delete()
+ .from('albums_tags')
+ .where({ 'albumId': albumId });
+
+ // Start deleting track associations with the album.
+ const deleteTracksPromise: Promise =
+ trx.delete()
+ .from('tracks_albums')
+ .where({ 'albumId': albumId });
+
+ // Start deleting the album.
+ const deleteAlbumPromise: Promise =
+ trx.delete()
+ .from('albums')
+ .where({ id: albumId });
+
+ // Wait for the requests to finish.
+ await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTracksPromise, deleteAlbumPromise]);
})
}
\ No newline at end of file
diff --git a/server/db/Artist.ts b/server/db/Artist.ts
index 94a1ac6..92c95cb 100644
--- a/server/db/Artist.ts
+++ b/server/db/Artist.ts
@@ -3,6 +3,7 @@ import { ArtistBaseWithRefs, ArtistWithDetails, ArtistWithRefs } from "../../cli
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
+var _ = require('lodash')
// Returns an artist with details, or null if not found.
export async function getArtist(id: number, userId: number, knex: Knex):
@@ -27,7 +28,18 @@ export async function getArtist(id: number, userId: number, knex: Knex):
.then((albums: any) => albums.map((album: any) => album['albumId']))
.then((ids: number[]) =>
knex.select(['id', 'name', 'storeLinks'])
- .from('album')
+ .from('albums')
+ .whereIn('id', ids)
+ );
+
+ const tracksPromise: Promise =
+ knex.select('trackId')
+ .from('tracks_artists')
+ .where({ 'artistId': id })
+ .then((tracks: any) => tracks.map((track: any) => track['trackId']))
+ .then((ids: number[]) =>
+ knex.select(['id', 'name', 'storeLinks'])
+ .from('tracks')
.whereIn('id', ids)
);
@@ -39,8 +51,8 @@ export async function getArtist(id: number, userId: number, knex: Knex):
.then((artists: any) => artists[0]);
// Wait for the requests to finish.
- const [artist, tags, albums] =
- await Promise.all([artistPromise, tagsPromise, albumsPromise]);
+ const [artist, tags, albums, tracks] =
+ await Promise.all([artistPromise, tagsPromise, albumsPromise, tracksPromise]);
if (artist) {
return {
@@ -48,6 +60,7 @@ export async function getArtist(id: number, userId: number, knex: Knex):
name: artist['name'],
albums: albums as api.AlbumWithId[],
tags: tags as api.TagWithId[],
+ tracks: tracks as api.TrackWithId[],
storeLinks: asJson(artist['storeLinks'] || []),
};
}
@@ -63,261 +76,307 @@ export async function getArtist(id: number, userId: number, knex: Knex):
// Returns the id of the created artist.
export async function createArtist(userId: number, artist: ArtistWithRefs, knex: Knex): Promise {
return await knex.transaction(async (trx) => {
- try {
- // Start retrieving albums.
- const albumIdsPromise: Promise =
- trx.select('id')
- .from('albums')
- .where({ 'user': userId })
- .whereIn('id', artist.albumIds)
- .then((as: any) => as.map((a: any) => a['id']));
+ // Start retrieving albums.
+ const albumIdsPromise: Promise =
+ trx.select('id')
+ .from('albums')
+ .where({ 'user': userId })
+ .whereIn('id', artist.albumIds || [])
+ .then((as: any) => as.map((a: any) => a['id']));
- // Start retrieving tags.
- const tagIdsPromise: Promise =
- trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .whereIn('id', artist.tagIds)
- .then((as: any) => as.map((a: any) => a['id']));
-
- // Wait for the requests to finish.
- var [albums, tags] = await Promise.all([albumIdsPromise, tagIdsPromise]);;
-
- // Check that we found all artists and tags we need.
- if ((new Set(albums.map((a: any) => a['id'])) !== new Set(artist.albumIds)) ||
- (new Set(tags.map((a: any) => a['id'])) !== new Set(artist.tagIds))) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Create the artist.
- const artistId = (await trx('artists')
- .insert({
- name: artist.name,
- storeLinks: JSON.stringify(artist.storeLinks || []),
- user: userId,
- })
- .returning('id') // Needed for Postgres
- )[0];
-
- // Link the albums via the linking table.
- if (albums && albums.length) {
- await trx('artists_albums').insert(
- albums.map((albumId: number) => {
- return {
- albumId: albumId,
- artistId: artistId,
- }
- })
- )
- }
-
- // Link the tags via the linking table.
- if (tags && tags.length) {
- await trx('artists_tags').insert(
- tags.map((tagId: number) => {
- return {
- artistId: artistId,
- tagId: tagId,
- }
- })
- )
- }
-
- return artistId;
-
- } catch (e) {
- trx.rollback();
+ // Start retrieving tracks.
+ const trackIdsPromise: Promise =
+ trx.select('id')
+ .from('tracks')
+ .where({ 'user': userId })
+ .whereIn('id', artist.trackIds || [])
+ .then((as: any) => as.map((a: any) => a['id']));
+
+ // Start retrieving tags.
+ const tagIdsPromise: Promise =
+ trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .whereIn('id', artist.tagIds || [])
+ .then((as: any) => as.map((a: any) => a['id']));
+
+ // Wait for the requests to finish.
+ var [albums, tags, tracks] = await Promise.all([albumIdsPromise, tagIdsPromise, trackIdsPromise]);;
+
+ // Check that we found all artists and tags we need.
+ if (!_.isEqual(albums.sort(), (artist.albumIds || []).sort()) ||
+ !_.isEqual(tags.sort(), (artist.tagIds || []).sort()) ||
+ !_.isEqual(tracks.sort(), (artist.trackIds || []).sort())) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
+ };
throw e;
}
+
+ // Create the artist.
+ const artistId = (await trx('artists')
+ .insert({
+ name: artist.name,
+ storeLinks: JSON.stringify(artist.storeLinks || []),
+ user: userId,
+ })
+ .returning('id') // Needed for Postgres
+ )[0];
+
+ // Link the albums via the linking table.
+ if (albums && albums.length) {
+ await trx('artists_albums').insert(
+ albums.map((albumId: number) => {
+ return {
+ albumId: albumId,
+ artistId: artistId,
+ }
+ })
+ )
+ }
+
+ // Link the tracks via the linking table.
+ if (tracks && tracks.length) {
+ await trx('tracks_artists').insert(
+ tracks.map((trackId: number) => {
+ return {
+ trackId: trackId,
+ artistId: artistId,
+ }
+ })
+ )
+ }
+
+ // Link the tags via the linking table.
+ if (tags && tags.length) {
+ await trx('artists_tags').insert(
+ tags.map((tagId: number) => {
+ return {
+ artistId: artistId,
+ tagId: tagId,
+ }
+ })
+ )
+ }
+
+ return artistId;
})
}
export async function modifyArtist(userId: number, artistId: number, artist: ArtistBaseWithRefs, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start retrieving the artist itself.
- const artistIdPromise: Promise =
- trx.select('id')
- .from('artists')
- .where({ 'user': userId })
- .where({ id: artistId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
-
- // Start retrieving albums if we are modifying those.
- const albumIdsPromise: Promise =
- artist.albumIds ?
- trx.select('albumId')
- .from('artists_albums')
- .whereIn('id', artist.albumIds)
- .then((as: any) => as.map((a: any) => a['albumId']))
- : (async () => undefined)();
-
- // Start retrieving tags if we are modifying those.
- const tagIdsPromise =
- artist.tagIds ?
- trx.select('id')
- .from('artists_tags')
- .whereIn('id', artist.tagIds)
- .then((ts: any) => ts.map((t: any) => t['tagId'])) :
- (async () => undefined)();
-
- // Wait for the requests to finish.
- var [oldArtist, albums, tags] = await Promise.all([artistIdPromise, albumIdsPromise, tagIdsPromise]);;
-
- // Check that we found all objects we need.
- if ((!albums || new Set(albums.map((a: any) => a['id'])) !== new Set(artist.albumIds)) ||
- (!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(artist.tagIds)) ||
- !oldArtist) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Modify the artist.
- var update: any = {};
- if ("name" in artist) { update["name"] = artist.name; }
- if ("storeLinks" in artist) { update["storeLinks"] = JSON.stringify(artist.storeLinks || []); }
-
- const modifyArtistPromise = trx('artists')
+ // Start retrieving the artist itself.
+ const artistIdPromise: Promise =
+ trx.select('id')
+ .from('artists')
.where({ 'user': userId })
- .where({ 'id': artistId })
- .update(update)
-
- // Remove unlinked albums.
- const removeUnlinkedAlbums = albums ? trx('artists_albums')
- .where({ 'artistId': artistId })
- .whereNotIn('albumId', artist.albumIds || [])
- .delete() : undefined;
-
- // Remove unlinked tags.
- const removeUnlinkedTags = tags ? trx('artists_tags')
- .where({ 'artistId': artistId })
- .whereNotIn('tagId', artist.tagIds || [])
- .delete() : undefined;
-
- // Link new albums.
- const addAlbums = albums ? trx('artists_albums')
- .where({ 'artistId': artistId })
- .then((as: any) => as.map((a: any) => a['albumId']))
- .then((doneAlbumIds: number[]) => {
- // Get the set of artists that are not yet linked
- const toLink = (albums || []).filter((id: number) => {
- return !doneAlbumIds.includes(id);
- });
- const insertObjects = toLink.map((albumId: number) => {
- return {
- artistId: artistId,
- albumId: albumId,
- }
- })
-
- // Link them
- return Promise.all(
- insertObjects.map((obj: any) =>
- trx('artists_artists').insert(obj)
- )
- );
- }) : undefined;
-
- // Link new tags.
- const addTags = tags ? trx('artists_tags')
- .where({ 'artistId': artistId })
- .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,
- artistId: artistId,
- }
- })
-
- // Link them
- return Promise.all(
- insertObjects.map((obj: any) =>
- trx('artists_tags').insert(obj)
- )
- );
- }) : undefined;
-
- // Wait for all operations to finish.
- await Promise.all([
- modifyArtistPromise,
- removeUnlinkedAlbums,
- removeUnlinkedTags,
- addAlbums,
- addTags
- ]);
-
- return;
-
- } catch (e) {
- trx.rollback();
+ .where({ id: artistId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
+
+ // Start retrieving albums if we are modifying those.
+ const albumIdsPromise: Promise =
+ artist.albumIds ?
+ trx.select('albumId')
+ .from('artists_albums')
+ .whereIn('id', artist.albumIds)
+ .then((as: any) => as.map((a: any) => a['albumId']))
+ : (async () => undefined)();
+
+ // Start retrieving tracks if we are modifying those.
+ const trackIdsPromise: Promise =
+ artist.trackIds ?
+ trx.select('trackId')
+ .from('tracks_artists')
+ .whereIn('id', artist.trackIds)
+ .then((as: any) => as.map((a: any) => a['trackId']))
+ : (async () => undefined)();
+
+ // Start retrieving tags if we are modifying those.
+ const tagIdsPromise =
+ artist.tagIds ?
+ trx.select('id')
+ .from('artists_tags')
+ .whereIn('id', artist.tagIds)
+ .then((ts: any) => ts.map((t: any) => t['tagId'])) :
+ (async () => undefined)();
+
+ // Wait for the requests to finish.
+ var [oldArtist, albums, tags, tracks] = await Promise.all([artistIdPromise, albumIdsPromise, tagIdsPromise, trackIdsPromise]);;
+
+ // Check that we found all objects we need.
+ if ((!albums || !_.isEqual(albums.sort(), (artist.albumIds || []).sort())) ||
+ (!tags || !_.isEqual(tags.sort(), (artist.tagIds || []).sort())) ||
+ (!tracks || !_.isEqual(tracks.sort(), (artist.trackIds || []).sort())) ||
+ !oldArtist) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
+ };
throw e;
}
+
+ // Modify the artist.
+ var update: any = {};
+ if ("name" in artist) { update["name"] = artist.name; }
+ if ("storeLinks" in artist) { update["storeLinks"] = JSON.stringify(artist.storeLinks || []); }
+
+ const modifyArtistPromise = trx('artists')
+ .where({ 'user': userId })
+ .where({ 'id': artistId })
+ .update(update)
+
+ // Remove unlinked albums.
+ const removeUnlinkedAlbums = albums ? trx('artists_albums')
+ .where({ 'artistId': artistId })
+ .whereNotIn('albumId', artist.albumIds || [])
+ .delete() : undefined;
+
+ // Remove unlinked tracks.
+ const removeUnlinkedTracks = tracks ? trx('tracks_artists')
+ .where({ 'artistId': artistId })
+ .whereNotIn('trackId', artist.trackIds || [])
+ .delete() : undefined;
+
+ // Remove unlinked tags.
+ const removeUnlinkedTags = tags ? trx('artists_tags')
+ .where({ 'artistId': artistId })
+ .whereNotIn('tagId', artist.tagIds || [])
+ .delete() : undefined;
+
+ // Link new albums.
+ const addAlbums = albums ? trx('artists_albums')
+ .where({ 'artistId': artistId })
+ .then((as: any) => as.map((a: any) => a['albumId']))
+ .then((doneAlbumIds: number[]) => {
+ // Get the set of artists that are not yet linked
+ const toLink = (albums || []).filter((id: number) => {
+ return !doneAlbumIds.includes(id);
+ });
+ const insertObjects = toLink.map((albumId: number) => {
+ return {
+ artistId: artistId,
+ albumId: albumId,
+ }
+ })
+
+ // Link them
+ return Promise.all(
+ insertObjects.map((obj: any) =>
+ trx('artists_albums').insert(obj)
+ )
+ );
+ }) : undefined;
+
+ // Link new tracks.
+ const addTracks = tracks ? trx('tracks_artists')
+ .where({ 'artistId': artistId })
+ .then((as: any) => as.map((a: any) => a['trackId']))
+ .then((doneTrackIds: number[]) => {
+ // Get the set of artists that are not yet linked
+ const toLink = (tracks || []).filter((id: number) => {
+ return !doneTrackIds.includes(id);
+ });
+ const insertObjects = toLink.map((trackId: number) => {
+ return {
+ artistId: artistId,
+ trackId: trackId,
+ }
+ })
+
+ // Link them
+ return Promise.all(
+ insertObjects.map((obj: any) =>
+ trx('tracks_artists').insert(obj)
+ )
+ );
+ }) : undefined;
+
+ // Link new tags.
+ const addTags = tags ? trx('artists_tags')
+ .where({ 'artistId': artistId })
+ .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,
+ artistId: artistId,
+ }
+ })
+
+ // Link them
+ return Promise.all(
+ insertObjects.map((obj: any) =>
+ trx('artists_tags').insert(obj)
+ )
+ );
+ }) : undefined;
+
+ // Wait for all operations to finish.
+ await Promise.all([
+ modifyArtistPromise,
+ removeUnlinkedAlbums,
+ removeUnlinkedTags,
+ removeUnlinkedTracks,
+ addAlbums,
+ addTags,
+ addTracks,
+ ]);
+
+ return;
})
}
export async function deleteArtist(userId: number, artistId: number, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start by retrieving the artist itself for sanity.
- const confirmArtistId: number | undefined =
- await trx.select('id')
- .from('artists')
- .where({ 'user': userId })
- .where({ id: artistId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
-
- if (!confirmArtistId) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all resources were found.',
- };
- throw e;
- }
-
- // Start deleting artist associations with the artist.
- const deleteAlbumsPromise: Promise =
- trx.delete()
- .from('artists_albums')
- .where({ 'artistId': artistId });
-
- // Start deleting tag associations with the artist.
- const deleteTagsPromise: Promise =
- trx.delete()
- .from('artists_tags')
- .where({ 'artistId': artistId });
-
- // Start deleting track associations with the artist.
- const deleteTracksPromise: Promise =
- trx.delete()
- .from('tracks_artists')
- .where({ 'artistId': artistId });
-
- // Start deleting the artist.
- const deleteArtistPromise: Promise =
- trx.delete()
- .from('artists')
- .where({ id: artistId });
-
- // Wait for the requests to finish.
- await Promise.all([deleteAlbumsPromise, deleteTagsPromise, deleteTracksPromise, deleteArtistPromise]);
- } catch (e) {
- trx.rollback();
+ // Start by retrieving the artist itself for sanity.
+ const confirmArtistId: number | undefined =
+ await trx.select('id')
+ .from('artists')
+ .where({ 'user': userId })
+ .where({ id: artistId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
+
+ if (!confirmArtistId) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all resources were found.',
+ };
throw e;
}
+
+ // Start deleting artist associations with the artist.
+ const deleteAlbumsPromise: Promise =
+ trx.delete()
+ .from('artists_albums')
+ .where({ 'artistId': artistId });
+
+ // Start deleting tag associations with the artist.
+ const deleteTagsPromise: Promise =
+ trx.delete()
+ .from('artists_tags')
+ .where({ 'artistId': artistId });
+
+ // Start deleting track associations with the artist.
+ const deleteTracksPromise: Promise =
+ trx.delete()
+ .from('tracks_artists')
+ .where({ 'artistId': artistId });
+
+ // Start deleting the artist.
+ const deleteArtistPromise: Promise =
+ trx.delete()
+ .from('artists')
+ .where({ id: artistId });
+
+ // Wait for the requests to finish.
+ await Promise.all([deleteAlbumsPromise, deleteTagsPromise, deleteTracksPromise, deleteArtistPromise]);
})
}
\ No newline at end of file
diff --git a/server/db/ImportExport.ts b/server/db/ImportExport.ts
index cb1f5b9..f9440f7 100644
--- a/server/db/ImportExport.ts
+++ b/server/db/ImportExport.ts
@@ -1,5 +1,5 @@
import Knex from "knex";
-import { TrackWithRefsWithId, AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs } from "../../client/src/api/api";
+import { TrackWithRefsWithId, AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs, AlbumBaseWithRefs } from "../../client/src/api/api";
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { createArtist } from "./Artist";
@@ -7,26 +7,12 @@ import { createTag } from "./Tag";
import { createAlbum } from "./Album";
import { createTrack } from "./Track";
-// This interface describes a JSON format in which the "interesting part"
-// of the entire database for a user can be imported/exported.
-// Worth noting is that the IDs used in this format only exist for cross-
-// referencing between objects. They do not correspond to IDs in the actual
-// database.
-// Upon import, they might be replaced, and upon export, they might be randomly
-// generated.
-interface DBImportExportFormat {
- tracks: TrackWithRefsWithId[],
- albums: AlbumWithRefsWithId[],
- artists: ArtistWithRefsWithId[],
- tags: TagWithRefsWithId[],
-}
-
-export async function exportDB(userId: number, knex: Knex): Promise {
+export async function exportDB(userId: number, knex: Knex): Promise {
// First, retrieve all the objects without taking linking tables into account.
// Fetch the links separately.
let tracksPromise: Promise =
- knex.select('id', 'name', 'storeLinks', 'albumId')
+ knex.select('id', 'name', 'storeLinks', 'album')
.from('tracks')
.where({ 'user': userId })
.then((ts: any[]) => ts.map((t: any) => {
@@ -35,7 +21,7 @@ export async function exportDB(userId: number, knex: Knex): Promise {
let [trackId, artistId] = v;
tracks.find((t: TrackWithRefsWithId) => t.id === trackId)?.artistIds.push(artistId);
+ artists.find((a: ArtistWithRefsWithId) => a.id === artistId)?.trackIds.push(trackId);
+ })
+ tracks.forEach((t: api.TrackWithRefsWithId) => {
+ albums.find((a: AlbumWithRefsWithId) => t.albumId && a.id === t.albumId)?.trackIds.push(t.id);
})
tracksTags.forEach((v: [number, number]) => {
let [trackId, tagId] = v;
@@ -164,41 +154,37 @@ export async function exportDB(userId: number, knex: Knex): Promise {
+export async function importDB(userId: number, db: api.DBImportExportFormat, knex: Knex): Promise {
return await knex.transaction(async (trx) => {
// Store the ID mappings in this record.
let tagIdMaps: Record = {};
let artistIdMaps: Record = {};
let albumIdMaps: Record = {};
let trackIdMaps: Record = {};
- try {
- // Insert items one by one, remapping the IDs as we go.
- await Promise.all(db.tags.map((tag: TagWithRefsWithId) => async () => {
- tagIdMaps[tag.id] = await createTag(userId, tag, knex);
- }));
- await Promise.all(db.artists.map((artist: ArtistWithRefsWithId) => async () => {
- artistIdMaps[artist.id] = await createArtist(userId, {
- ...artist,
- tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]),
- }, knex);
- }))
- await Promise.all(db.albums.map((album: AlbumWithRefsWithId) => async () => {
- albumIdMaps[album.id] = await createAlbum(userId, {
- ...album,
- tagIds: album.tagIds.map((id: number) => tagIdMaps[id]),
- artistIds: album.artistIds.map((id: number) => artistIdMaps[id]),
- }, knex);
- }))
- await Promise.all(db.tracks.map((track: TrackWithRefsWithId) => async () => {
- trackIdMaps[track.id] = await createTrack(userId, {
- ...track,
- tagIds: track.tagIds.map((id: number) => tagIdMaps[id]),
- artistIds: track.artistIds.map((id: number) => artistIdMaps[id]),
- albumId: track.albumId ? albumIdMaps[track.albumId] : null,
- }, knex);
- }))
- } catch (e) {
- trx.rollback();
- }
+ // Insert items one by one, remapping the IDs as we go.
+ await Promise.all(db.tags.map((tag: TagWithRefsWithId) => async () => {
+ tagIdMaps[tag.id] = await createTag(userId, tag, knex);
+ }));
+ await Promise.all(db.artists.map((artist: ArtistWithRefsWithId) => async () => {
+ artistIdMaps[artist.id] = await createArtist(userId, {
+ ...artist,
+ tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]),
+ }, knex);
+ }))
+ await Promise.all(db.albums.map((album: AlbumWithRefsWithId) => async () => {
+ albumIdMaps[album.id] = await createAlbum(userId, {
+ ...album,
+ tagIds: album.tagIds.map((id: number) => tagIdMaps[id]),
+ artistIds: album.artistIds.map((id: number) => artistIdMaps[id]),
+ }, knex);
+ }))
+ await Promise.all(db.tracks.map((track: TrackWithRefsWithId) => async () => {
+ trackIdMaps[track.id] = await createTrack(userId, {
+ ...track,
+ tagIds: track.tagIds.map((id: number) => tagIdMaps[id]),
+ artistIds: track.artistIds.map((id: number) => artistIdMaps[id]),
+ albumId: track.albumId ? albumIdMaps[track.albumId] : null,
+ }, knex);
+ }))
});
}
\ No newline at end of file
diff --git a/server/db/Integration.ts b/server/db/Integration.ts
index 0d13e9f..f5a426d 100644
--- a/server/db/Integration.ts
+++ b/server/db/Integration.ts
@@ -6,25 +6,20 @@ import { IntegrationDataWithId, IntegrationDataWithSecret, PartialIntegrationDat
export async function createIntegration(userId: number, integration: api.IntegrationDataWithSecret, knex: Knex): Promise {
return await knex.transaction(async (trx) => {
- try {
- // Create the new integration.
- var integration: any = {
- name: integration.name,
- user: userId,
- type: integration.type,
- details: JSON.stringify(integration.details),
- secretDetails: JSON.stringify(integration.secretDetails),
- }
- const integrationId = (await trx('integrations')
- .insert(integration)
- .returning('id') // Needed for Postgres
- )[0];
-
- return integrationId;
- } catch (e) {
- trx.rollback();
- throw e;
+ // Create the new integration.
+ var integration: any = {
+ name: integration.name,
+ user: userId,
+ type: integration.type,
+ details: JSON.stringify(integration.details),
+ secretDetails: JSON.stringify(integration.secretDetails),
}
+ const integrationId = (await trx('integrations')
+ .insert(integration)
+ .returning('id') // Needed for Postgres
+ )[0];
+
+ return integrationId;
})
}
@@ -71,65 +66,56 @@ export async function listIntegrations(userId: number, knex: Knex): Promise {
- try {
- // Start retrieving the integration itself.
- const integrationId = await trx.select('id')
- .from('integrations')
- .where({ 'user': userId })
- .where({ id: id })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
-
- // Check that we found all objects we need.
- if (!integrationId) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: "Resource not found."
- };
- throw e;
- }
-
- // Delete the integration.
- await trx('integrations')
- .where({ 'user': userId, 'id': integrationId })
- .del();
+ // Start retrieving the integration itself.
+ const integrationId = await trx.select('id')
+ .from('integrations')
+ .where({ 'user': userId })
+ .where({ id: id })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
- } catch (e) {
- trx.rollback();
+ // Check that we found all objects we need.
+ if (!integrationId) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: "Resource not found."
+ };
+ throw e;
}
+
+ // Delete the integration.
+ await trx('integrations')
+ .where({ 'user': userId, 'id': integrationId })
+ .del();
})
}
export async function modifyIntegration(userId: number, id: number, integration: PartialIntegrationData, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start retrieving the integration.
- const integrationId = await trx.select('id')
- .from('integrations')
- .where({ 'user': userId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
-
- // Check that we found all objects we need.
- if (!integrationId) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: "Resource not found",
- };
- throw e;
- }
+ // Start retrieving the integration.
+ const integrationId = await trx.select('id')
+ .from('integrations')
+ .where({ 'user': userId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
- // Modify the integration.
- var update: any = {};
- if ("name" in integration) { update["name"] = integration.name; }
- if ("details" in integration) { update["details"] = JSON.stringify(integration.details); }
- if ("type" in integration) { update["type"] = integration.type; }
- if ("secretDetails" in integration) { update["secretDetails"] = JSON.stringify(integration.details); }
- await trx('integrations')
- .where({ 'user': userId, 'id': id })
- .update(update)
- } catch (e) {
- trx.rollback();
+ // Check that we found all objects we need.
+ if (!integrationId) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: "Resource not found",
+ };
+ throw e;
}
+
+ // Modify the integration.
+ var update: any = {};
+ if ("name" in integration) { update["name"] = integration.name; }
+ if ("details" in integration) { update["details"] = JSON.stringify(integration.details); }
+ if ("type" in integration) { update["type"] = integration.type; }
+ if ("secretDetails" in integration) { update["secretDetails"] = JSON.stringify(integration.details); }
+ await trx('integrations')
+ .where({ 'user': userId, 'id': id })
+ .update(update)
})
}
\ No newline at end of file
diff --git a/server/db/Query.ts b/server/db/Query.ts
index 69120c3..c7ad76c 100644
--- a/server/db/Query.ts
+++ b/server/db/Query.ts
@@ -22,7 +22,7 @@ export function toApiArtist(dbObj: any): api.Artist {
};
}
-export function toApiTrack(dbObj: any, artists: any[], tags: any[], album: any | undefined): api.Track {
+export function toApiTrack(dbObj: any, artists: any[], tags: any[], albums: any[]): api.Track {
return {
mbApi_typename: "track",
trackId: dbObj['tracks.id'],
@@ -34,7 +34,7 @@ export function toApiTrack(dbObj: any, artists: any[], tags: any[], album: any |
tags: tags.map((tag: any) => {
return toApiTag(tag);
}),
- album: album,
+ album: albums.length > 0 ? toApiAlbum(albums[0]) : null,
}
}
@@ -80,23 +80,34 @@ const objectTables: Record = {
// To keep track of linking tables between objects.
const linkingTables: any = [
- [[ObjectType.Track, ObjectType.Album], 'tracks_albums'],
[[ObjectType.Track, ObjectType.Artist], 'tracks_artists'],
[[ObjectType.Track, ObjectType.Tag], 'tracks_tags'],
[[ObjectType.Artist, ObjectType.Album], 'artists_albums'],
[[ObjectType.Artist, ObjectType.Tag], 'artists_tags'],
[[ObjectType.Album, ObjectType.Tag], 'albums_tags'],
]
-function getLinkingTable(a: ObjectType, b: ObjectType): string {
+function getLinkingTable(a: ObjectType, b: ObjectType): string | undefined {
var res: string | undefined = undefined;
linkingTables.forEach((row: any) => {
if (row[0].includes(a) && row[0].includes(b)) {
res = row[1];
}
})
- if (res) return res;
+ return res;
+}
- throw "Could not find linking table for objects: " + JSON.stringify(a) + ", " + JSON.stringify(b);
+// To keep track of linking columns between objects.
+const linkingColumns: any = [
+ [[ObjectType.Track, ObjectType.Album], 'tracks.album'],
+]
+function getLinkingColumn(a: ObjectType, b: ObjectType): string | undefined {
+ var res: string | undefined = undefined;
+ linkingColumns.forEach((row: any) => {
+ if (row[0].includes(a) && row[0].includes(b)) {
+ res = row[1];
+ }
+ })
+ return res;
}
// To keep track of ID fields used in linking tables.
@@ -124,11 +135,18 @@ function getRequiredDatabaseObjects(queryElem: api.QueryElem): Set {
function addJoin(knexQuery: any, base: ObjectType, other: ObjectType) {
const linkTable = getLinkingTable(base, other);
+ const linkColumn = getLinkingColumn(base, other);
const baseTable = objectTables[base];
const otherTable = objectTables[other];
- return knexQuery
- .join(linkTable, { [baseTable + '.id']: linkTable + '.' + linkingTableIdNames[base] })
- .join(otherTable, { [otherTable + '.id']: linkTable + '.' + linkingTableIdNames[other] });
+
+ if (linkTable) {
+ return knexQuery
+ .join(linkTable, { [baseTable + '.id']: linkTable + '.' + linkingTableIdNames[base] })
+ .join(otherTable, { [otherTable + '.id']: linkTable + '.' + linkingTableIdNames[other] });
+ } else if (linkColumn) {
+ return knexQuery
+ .join(otherTable, { [linkColumn]: otherTable + '.id' });
+ }
}
enum WhereType {
@@ -231,7 +249,7 @@ function getWhere(queryElem: api.QueryElem): string {
}
const objectColumns = {
- [ObjectType.Track]: ['tracks.id as tracks.id', 'tracks.title as tracks.title', 'tracks.storeLinks as tracks.storeLinks'],
+ [ObjectType.Track]: ['tracks.id as tracks.id', 'tracks.name as tracks.name', 'tracks.storeLinks as tracks.storeLinks', 'tracks.album as tracks.album'],
[ObjectType.Artist]: ['artists.id as artists.id', 'artists.name as artists.name', 'artists.storeLinks as artists.storeLinks'],
[ObjectType.Album]: ['albums.id as albums.id', 'albums.name as albums.name', 'albums.storeLinks as albums.storeLinks'],
[ObjectType.Tag]: ['tags.id as tags.id', 'tags.name as tags.name', 'tags.parentId as tags.parentId']
@@ -267,7 +285,7 @@ function constructQuery(knex: Knex, userId: number, queryFor: ObjectType, queryE
// Apply ordering
const orderKeys = {
- [api.OrderByType.Name]: objectTables[queryFor] + '.' + ((queryFor === ObjectType.Track) ? 'title' : 'name')
+ [api.OrderByType.Name]: objectTables[queryFor] + '.' + ((queryFor === ObjectType.Track) ? 'name' : 'name')
};
q = q.orderBy(orderKeys[ordering.orderBy.type],
(ordering.ascending ? 'asc' : 'desc'));
@@ -285,17 +303,33 @@ function constructQuery(knex: Knex, userId: number, queryFor: ObjectType, queryE
async function getLinkedObjects(knex: Knex, userId: number, base: ObjectType, linked: ObjectType, baseIds: number[]) {
var result: Record = {};
+ const table = objectTables[base];
const otherTable = objectTables[linked];
- const linkingTable = getLinkingTable(base, linked);
+ const maybeLinkingTable = getLinkingTable(base, linked);
+ const maybeLinkingColumn = getLinkingColumn(base, linked);
const columns = objectColumns[linked];
- await Promise.all(baseIds.map((baseId: number) => {
- return knex.select(columns).groupBy(otherTable + '.id').from(otherTable)
- .join(linkingTable, { [linkingTable + '.' + linkingTableIdNames[linked]]: otherTable + '.id' })
- .where({ [otherTable + '.user']: userId })
- .where({ [linkingTable + '.' + linkingTableIdNames[base]]: baseId })
- .then((others: any) => { result[baseId] = others; })
- }))
+ console.log(table, otherTable, maybeLinkingTable, maybeLinkingColumn);
+
+ if (maybeLinkingTable) {
+ await Promise.all(baseIds.map((baseId: number) => {
+ return knex.select(columns).groupBy(otherTable + '.id').from(otherTable)
+ .join(maybeLinkingTable, { [maybeLinkingTable + '.' + linkingTableIdNames[linked]]: otherTable + '.id' })
+ .where({ [otherTable + '.user']: userId })
+ .where({ [maybeLinkingTable + '.' + linkingTableIdNames[base]]: baseId })
+ .then((others: any) => { result[baseId] = others; })
+ }))
+ } else if (maybeLinkingColumn) {
+ await Promise.all(baseIds.map((baseId: number) => {
+ return knex.select(columns).groupBy(otherTable + '.id').from(otherTable)
+ .join(table, { [maybeLinkingColumn]: otherTable + '.id' })
+ .where({ [otherTable + '.user']: userId })
+ .where({ [table + '.id']: baseId })
+ .then((others: any) => { result[baseId] = others; })
+ }))
+ } else {
+ throw new Error('canno link objects.')
+ }
console.log("Query results for", baseIds, ":", result);
return result;
@@ -344,7 +378,7 @@ export async function doQuery(userId: number, q: api.QueryRequest, knex: Knex):
ObjectType.Album,
q.query,
q.ordering,
- artistOffset || 0,
+ albumOffset || 0,
albumLimit >= 0 ? albumLimit : null,
) :
(async () => [])();
@@ -472,5 +506,6 @@ export async function doQuery(userId: number, q: api.QueryRequest, knex: Knex):
}
}
+ console.log("Query response:", response)
return response;
}
\ No newline at end of file
diff --git a/server/db/Tag.ts b/server/db/Tag.ts
index 8be6127..3a8e01a 100644
--- a/server/db/Tag.ts
+++ b/server/db/Tag.ts
@@ -1,4 +1,5 @@
import Knex from "knex";
+import { isConstructorDeclaration } from "typescript";
import * as api from '../../client/src/api/api';
import { TagBaseWithRefs, TagWithDetails, TagWithId, TagWithRefs, TagWithRefsWithId } from "../../client/src/api/api";
import { DBError, DBErrorKind } from "../endpoints/types";
@@ -24,109 +25,98 @@ export async function getTagChildrenRecursive(id: number, userId: number, trx: a
// Returns the id of the created tag.
export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): Promise {
return await knex.transaction(async (trx) => {
- try {
- // If applicable, retrieve the parent tag.
- const maybeParent: number | null =
- tag.parentId ?
- (await trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .where({ 'id': tag.parentId }))[0]['id'] :
- null;
-
- // Check if the parent was found, if applicable.
- if (tag.parentId && maybeParent !== tag.parentId) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Create the new tag.
- var tag: any = {
- name: tag.name,
- user: userId,
+ // If applicable, retrieve the parent tag.
+ const maybeParent: number | null =
+ tag.parentId ?
+ (await trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .where({ 'id': tag.parentId }))[0]['id'] :
+ null;
+
+ // Check if the parent was found, if applicable.
+ if (tag.parentId && maybeParent !== tag.parentId) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
};
- if (maybeParent) {
- tag['parentId'] = maybeParent;
- }
- const tagId = (await trx('tags')
- .insert(tag)
- .returning('id') // Needed for Postgres
- )[0];
-
- return tagId;
-
- } catch (e) {
- trx.rollback();
throw e;
}
+
+ // Create the new tag.
+ var newTag: any = {
+ name: tag.name,
+ user: userId,
+ };
+ if (maybeParent) {
+ newTag['parentId'] = maybeParent;
+ }
+ const tagId = (await trx('tags')
+ .insert(newTag)
+ .returning('id') // Needed for Postgres
+ )[0];
+
+ return tagId;
})
}
export async function deleteTag(userId: number, tagId: number, knex: Knex) {
await knex.transaction(async (trx) => {
- try {
- // Start retrieving any child tags.
- const childTagsPromise =
- getTagChildrenRecursive(tagId, userId, trx);
+ // Start retrieving any child tags.
+ const childTagsPromise =
+ getTagChildrenRecursive(tagId, userId, trx);
- // Start retrieving the tag itself.
- const tagPromise = trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .where({ id: tagId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
-
- // Wait for the requests to finish.
- var [tag, children] = await Promise.all([tagPromise, childTagsPromise]);
-
- // Merge all IDs.
- const toDelete = [tag, ...children];
-
- // Check that we found all objects we need.
- if (!tag) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Start deleting artist associations with the tag.
- const deleteArtistsPromise: Promise =
- trx.delete()
- .from('artists_tags')
- .whereIn('tagId', toDelete);
-
- // Start deleting album associations with the tag.
- const deleteAlbumsPromise: Promise =
- trx.delete()
- .from('albums_tags')
- .whereIn('tagId', toDelete);
-
- // Start deleting track associations with the tag.
- const deleteTracksPromise: Promise =
- trx.delete()
- .from('tracks_tags')
- .whereIn('tagId', toDelete);
-
-
- // Start deleting the tag and its children.
- const deleteTags: Promise = trx('tags')
- .where({ 'user': userId })
- .whereIn('id', toDelete)
- .del();
+ // Start retrieving the tag itself.
+ const tagPromise = trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .where({ id: tagId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
- await Promise.all([deleteArtistsPromise, deleteAlbumsPromise, deleteTracksPromise, deleteTags])
- } catch (e) {
- trx.rollback();
+ // Wait for the requests to finish.
+ var [tag, children] = await Promise.all([tagPromise, childTagsPromise]);
+
+ // Merge all IDs.
+ const toDelete = [tag, ...children];
+
+ // Check that we found all objects we need.
+ if (!tag) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
+ };
throw e;
}
+
+ // Start deleting artist associations with the tag.
+ const deleteArtistsPromise: Promise =
+ trx.delete()
+ .from('artists_tags')
+ .whereIn('tagId', toDelete);
+
+ // Start deleting album associations with the tag.
+ const deleteAlbumsPromise: Promise =
+ trx.delete()
+ .from('albums_tags')
+ .whereIn('tagId', toDelete);
+
+ // Start deleting track associations with the tag.
+ const deleteTracksPromise: Promise =
+ trx.delete()
+ .from('tracks_tags')
+ .whereIn('tagId', toDelete);
+
+
+ // Start deleting the tag and its children.
+ const deleteTags: Promise = trx('tags')
+ .where({ 'user': userId })
+ .whereIn('id', toDelete)
+ .del();
+
+ await Promise.all([deleteArtistsPromise, deleteAlbumsPromise, deleteTracksPromise, deleteTags])
})
}
@@ -168,107 +158,96 @@ export async function getTag(userId: number, tagId: number, knex: Knex): Promise
export async function modifyTag(userId: number, tagId: number, tag: TagBaseWithRefs, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start retrieving the parent tag.
- const parentTagIdPromise: Promise = tag.parentId ?
- trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .where({ 'id': tag.parentId })
- .then((ts: any) => ts.map((t: any) => t['tagId'])) :
- (async () => { return null })();
-
- // Start retrieving the tag itself.
- const tagPromise = trx.select('id')
+ // Start retrieving the parent tag.
+ const parentTagIdPromise: Promise = tag.parentId ?
+ trx.select('id')
.from('tags')
.where({ 'user': userId })
- .where({ id: tagId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
-
- // Wait for the requests to finish.
- var [dbTag, parent] = await Promise.all([tagPromise, parentTagIdPromise]);
-
- // Check that we found all objects we need.
- if ((tag.parentId && !parent) ||
- !dbTag) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all resources were found.',
- };
- throw e;
- }
-
- // Modify the tag.
- await trx('tags')
- .where({ 'user': userId })
- .where({ 'id': tagId })
- .update({
- name: tag.name,
- parentId: tag.parentId || null,
- })
-
- } catch (e) {
- trx.rollback();
+ .where({ 'id': tag.parentId })
+ .then((ts: any) => ts.map((t: any) => t['tagId'])) :
+ (async () => { return null })();
+
+ // Start retrieving the tag itself.
+ const tagPromise = trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .where({ id: tagId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
+
+ // Wait for the requests to finish.
+ var [dbTag, parent] = await Promise.all([tagPromise, parentTagIdPromise]);
+
+ // Check that we found all objects we need.
+ if ((tag.parentId && !parent) ||
+ !dbTag) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all resources were found.',
+ };
throw e;
}
+
+ // Modify the tag.
+ await trx('tags')
+ .where({ 'user': userId })
+ .where({ 'id': tagId })
+ .update({
+ name: tag.name,
+ parentId: tag.parentId || null,
+ })
})
}
export async function mergeTag(userId: number, fromId: number, toId: number, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start retrieving the "from" tag.
- const fromTagIdPromise = trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .where({ id: fromId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
+ // Start retrieving the "from" tag.
+ const fromTagIdPromise = trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .where({ id: fromId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
- // Start retrieving the "to" tag.
- const toTagIdPromise = trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .where({ id: toId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
-
- // Wait for the requests to finish.
- var [fromTagId, toTagId] = await Promise.all([fromTagIdPromise, toTagIdPromise]);
-
- // Check that we found all objects we need.
- if (!fromTagId || !toTagId) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all resources were found.',
- };
- throw e;
- }
-
- // Assign new tag ID to any objects referencing the to-be-merged tag.
- const cPromise = trx('tags')
- .where({ 'user': userId })
- .where({ 'parentId': fromId })
- .update({ 'parentId': toId });
- const sPromise = trx('songs_tags')
- .where({ 'tagId': fromId })
- .update({ 'tagId': toId });
- const arPromise = trx('artists_tags')
- .where({ 'tagId': fromId })
- .update({ 'tagId': toId });
- const alPromise = trx('albums_tags')
- .where({ 'tagId': fromId })
- .update({ 'tagId': toId });
- await Promise.all([sPromise, arPromise, alPromise, cPromise]);
-
- // Delete the original tag.
- await trx('tags')
- .where({ 'user': userId })
- .where({ 'id': fromId })
- .del();
- } catch (e) {
- trx.rollback();
+ // Start retrieving the "to" tag.
+ const toTagIdPromise = trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .where({ id: toId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined)
+
+ // Wait for the requests to finish.
+ var [fromTagId, toTagId] = await Promise.all([fromTagIdPromise, toTagIdPromise]);
+
+ // Check that we found all objects we need.
+ if (!fromTagId || !toTagId) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all resources were found.',
+ };
throw e;
}
+
+ // Assign new tag ID to any objects referencing the to-be-merged tag.
+ const cPromise = trx('tags')
+ .where({ 'user': userId })
+ .where({ 'parentId': fromId })
+ .update({ 'parentId': toId });
+ const sPromise = trx('songs_tags')
+ .where({ 'tagId': fromId })
+ .update({ 'tagId': toId });
+ const arPromise = trx('artists_tags')
+ .where({ 'tagId': fromId })
+ .update({ 'tagId': toId });
+ const alPromise = trx('albums_tags')
+ .where({ 'tagId': fromId })
+ .update({ 'tagId': toId });
+ await Promise.all([sPromise, arPromise, alPromise, cPromise]);
+
+ // Delete the original tag.
+ await trx('tags')
+ .where({ 'user': userId })
+ .where({ 'id': fromId })
+ .del();
})
}
\ No newline at end of file
diff --git a/server/db/Track.ts b/server/db/Track.ts
index e693424..bee6dff 100644
--- a/server/db/Track.ts
+++ b/server/db/Track.ts
@@ -3,6 +3,7 @@ import { TrackBaseWithRefs, TrackWithDetails, TrackWithRefs } from "../../client
import * as api from '../../client/src/api/api';
import asJson from "../lib/asJson";
import { DBError, DBErrorKind } from "../endpoints/types";
+var _ = require('lodash')
// Returns an track with details, or null if not found.
export async function getTrack(id: number, userId: number, knex: Knex):
@@ -76,268 +77,253 @@ export async function getTrack(id: number, userId: number, knex: Knex):
// Returns the id of the created track.
export async function createTrack(userId: number, track: TrackWithRefs, knex: Knex): Promise {
return await knex.transaction(async (trx) => {
- try {
- // Start retrieving artists.
- const artistIdsPromise: Promise =
- trx.select('id')
- .from('artists')
- .where({ 'user': userId })
- .whereIn('id', track.artistIds)
- .then((as: any) => as.map((a: any) => a['id']));
+ // Start retrieving artists.
+ const artistIdsPromise: Promise =
+ trx.select('id')
+ .from('artists')
+ .where({ 'user': userId })
+ .whereIn('id', track.artistIds)
+ .then((as: any) => as.map((a: any) => a['id']));
- // Start retrieving tags.
- const tagIdsPromise: Promise =
- trx.select('id')
- .from('tags')
- .where({ 'user': userId })
- .whereIn('id', track.tagIds)
- .then((as: any) => as.map((a: any) => a['id']));
+ // Start retrieving tags.
+ const tagIdsPromise: Promise =
+ trx.select('id')
+ .from('tags')
+ .where({ 'user': userId })
+ .whereIn('id', track.tagIds)
+ .then((as: any) => as.map((a: any) => a['id']));
- // Start retrieving album.
- const albumIdPromise: Promise =
- knex.select('id')
+ // Start retrieving album.
+ const albumIdPromise: Promise =
+ track.albumId ?
+ trx.select('id')
.from('albums')
- .where({ 'user': userId, 'albumId': track.albumId })
- .then((albums: any) => albums.map((album: any) => album['albumId']))
+ .where({ 'user': userId, 'id': track.albumId })
+ .then((albums: any) => albums.map((album: any) => album['id']))
.then((ids: number[]) =>
ids.length > 0 ? ids[0] : (() => null)()
- );
-
- // Wait for the requests to finish.
- var [artists, tags, album] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdPromise]);;
-
- // Check that we found all artists and tags we need.
- if ((new Set((artists as number[]).map((a: any) => a['id'])) !== new Set(track.artistIds)) ||
- (new Set((tags as number[]).map((a: any) => a['id'])) !== new Set(track.tagIds)) ||
- (album === null)) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Create the track.
- const trackId = (await trx('tracks')
- .insert({
- name: track.name,
- storeLinks: JSON.stringify(track.storeLinks || []),
- user: userId,
- albumId: album,
- })
- .returning('id') // Needed for Postgres
- )[0];
-
- // Link the artists via the linking table.
- if (artists && artists.length) {
- await trx('artists_tracks').insert(
- artists.map((artistId: number) => {
- return {
- artistId: artistId,
- trackId: trackId,
- }
- })
- )
- }
-
- // Link the tags via the linking table.
- if (tags && tags.length) {
- await trx('tracks_tags').insert(
- tags.map((tagId: number) => {
- return {
- trackId: trackId,
- tagId: tagId,
- }
- })
- )
- }
-
- return trackId;
-
- } catch (e) {
- trx.rollback();
+ ) :
+ (async () => null)();
+
+ // Wait for the requests to finish.
+ var [artists, tags, album] = await Promise.all([artistIdsPromise, tagIdsPromise, albumIdPromise]);
+
+ // Check that we found all artists and tags we need.
+ if (!_.isEqual((artists as number[]).sort(), track.artistIds.sort()) ||
+ (!_.isEqual((tags as number[]).sort(), track.tagIds.sort())) ||
+ (track.albumId && (album === null))) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
+ };
throw e;
}
+
+ // Create the track.
+ const trackId = (await trx('tracks')
+ .insert({
+ name: track.name,
+ storeLinks: JSON.stringify(track.storeLinks || []),
+ user: userId,
+ album: album || null,
+ })
+ .returning('id') // Needed for Postgres
+ )[0];
+
+ // Link the artists via the linking table.
+ if (artists && artists.length) {
+ await trx('tracks_artists').insert(
+ artists.map((artistId: number) => {
+ return {
+ artistId: artistId,
+ trackId: trackId,
+ }
+ })
+ )
+ }
+
+ // Link the tags via the linking table.
+ if (tags && tags.length) {
+ await trx('tracks_tags').insert(
+ tags.map((tagId: number) => {
+ return {
+ trackId: trackId,
+ tagId: tagId,
+ }
+ })
+ )
+ }
+
+ return trackId;
})
}
export async function modifyTrack(userId: number, trackId: number, track: TrackBaseWithRefs, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start retrieving the track itself.
- const trackIdPromise: Promise =
- trx.select('id')
- .from('tracks')
- .where({ 'user': userId })
- .where({ id: trackId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
-
- // Start retrieving artists if we are modifying those.
- const artistIdsPromise: Promise =
- track.artistIds ?
- trx.select('artistId')
- .from('artists_tracks')
- .whereIn('artistId', track.artistIds)
- .then((as: any) => as.map((a: any) => a['artistId']))
- : (async () => undefined)();
-
- // Start retrieving tags if we are modifying those.
- const tagIdsPromise =
- track.tagIds ?
- trx.select('id')
- .from('tracks_tags')
- .whereIn('tagId', track.tagIds)
- .then((ts: any) => ts.map((t: any) => t['tagId'])) :
- (async () => undefined)();
-
- // Wait for the requests to finish.
- var [oldTrack, artists, tags] = await Promise.all([trackIdPromise, artistIdsPromise, tagIdsPromise]);;
-
- // Check that we found all objects we need.
- if ((!artists || new Set(artists.map((a: any) => a['id'])) !== new Set(track.artistIds)) ||
- (!tags || new Set(tags.map((a: any) => a['id'])) !== new Set(track.tagIds)) ||
- !oldTrack) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all to-be-linked resources were found.',
- };
- throw e;
- }
-
- // Modify the track.
- var update: any = {};
- if ("name" in track) { update["name"] = track.name; }
- if ("storeLinks" in track) { update["storeLinks"] = JSON.stringify(track.storeLinks || []); }
- if ("albumId" in track) { update["albumId"] = track.albumId; }
-
- const modifyTrackPromise = trx('tracks')
+ // Start retrieving the track itself.
+ const trackIdPromise: Promise =
+ trx.select('id')
+ .from('tracks')
.where({ 'user': userId })
- .where({ 'id': trackId })
- .update(update)
-
- // Remove unlinked artists.
- const removeUnlinkedArtists = artists ? trx('artists_tracks')
- .where({ 'trackId': trackId })
- .whereNotIn('artistId', track.artistIds || [])
- .delete() : undefined;
-
- // Remove unlinked tags.
- const removeUnlinkedTags = tags ? trx('tracks_tags')
- .where({ 'trackId': trackId })
- .whereNotIn('tagId', track.tagIds || [])
- .delete() : undefined;
-
- // Link new artists.
- const addArtists = artists ? trx('artists_tracks')
- .where({ 'trackId': trackId })
- .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,
- trackId: trackId,
- }
- })
-
- // Link them
- return Promise.all(
- insertObjects.map((obj: any) =>
- trx('artists_tracks').insert(obj)
- )
- );
- }) : undefined;
-
- // Link new tags.
- const addTags = tags ? trx('tracks_tags')
- .where({ 'trackId': trackId })
- .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,
- trackId: trackId,
- }
- })
-
- // Link them
- return Promise.all(
- insertObjects.map((obj: any) =>
- trx('tracks_tags').insert(obj)
- )
- );
- }) : undefined;
-
- // Wait for all operations to finish.
- await Promise.all([
- modifyTrackPromise,
- removeUnlinkedArtists,
- removeUnlinkedTags,
- addArtists,
- addTags,
- ]);
-
- return;
-
- } catch (e) {
- trx.rollback();
+ .where({ id: trackId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
+
+ // Start retrieving artists if we are modifying those.
+ const artistIdsPromise: Promise =
+ track.artistIds ?
+ trx.select('artistId')
+ .from('artists_tracks')
+ .whereIn('artistId', track.artistIds)
+ .then((as: any) => as.map((a: any) => a['artistId']))
+ : (async () => undefined)();
+
+ // Start retrieving tags if we are modifying those.
+ const tagIdsPromise =
+ track.tagIds ?
+ trx.select('id')
+ .from('tracks_tags')
+ .whereIn('tagId', track.tagIds)
+ .then((ts: any) => ts.map((t: any) => t['tagId'])) :
+ (async () => undefined)();
+
+ // Wait for the requests to finish.
+ var [oldTrack, artists, tags] = await Promise.all([trackIdPromise, artistIdsPromise, tagIdsPromise]);;
+
+ // Check that we found all objects we need.
+ if ((!artists || !_.isEqual(artists.sort(), (track.artistIds || []).sort())) ||
+ (!tags || !_.isEqual(tags.sort(), (track.tagIds || []).sort())) ||
+ !oldTrack) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all to-be-linked resources were found.',
+ };
throw e;
}
+
+ // Modify the track.
+ var update: any = {};
+ if ("name" in track) { update["name"] = track.name; }
+ if ("storeLinks" in track) { update["storeLinks"] = JSON.stringify(track.storeLinks || []); }
+ if ("albumId" in track) { update["albumId"] = track.albumId; }
+
+ const modifyTrackPromise = trx('tracks')
+ .where({ 'user': userId })
+ .where({ 'id': trackId })
+ .update(update)
+
+ // Remove unlinked artists.
+ const removeUnlinkedArtists = artists ? trx('artists_tracks')
+ .where({ 'trackId': trackId })
+ .whereNotIn('artistId', track.artistIds || [])
+ .delete() : undefined;
+
+ // Remove unlinked tags.
+ const removeUnlinkedTags = tags ? trx('tracks_tags')
+ .where({ 'trackId': trackId })
+ .whereNotIn('tagId', track.tagIds || [])
+ .delete() : undefined;
+
+ // Link new artists.
+ const addArtists = artists ? trx('artists_tracks')
+ .where({ 'trackId': trackId })
+ .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,
+ trackId: trackId,
+ }
+ })
+
+ // Link them
+ return Promise.all(
+ insertObjects.map((obj: any) =>
+ trx('artists_tracks').insert(obj)
+ )
+ );
+ }) : undefined;
+
+ // Link new tags.
+ const addTags = tags ? trx('tracks_tags')
+ .where({ 'trackId': trackId })
+ .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,
+ trackId: trackId,
+ }
+ })
+
+ // Link them
+ return Promise.all(
+ insertObjects.map((obj: any) =>
+ trx('tracks_tags').insert(obj)
+ )
+ );
+ }) : undefined;
+
+ // Wait for all operations to finish.
+ await Promise.all([
+ modifyTrackPromise,
+ removeUnlinkedArtists,
+ removeUnlinkedTags,
+ addArtists,
+ addTags,
+ ]);
+
+ return;
})
}
export async function deleteTrack(userId: number, trackId: number, knex: Knex): Promise {
await knex.transaction(async (trx) => {
- try {
- // Start by retrieving the track itself for sanity.
- const confirmTrackId: number | undefined =
- await trx.select('id')
- .from('tracks')
- .where({ 'user': userId })
- .where({ id: trackId })
- .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
-
- if (!confirmTrackId) {
- const e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceNotFound,
- message: 'Not all resources were found.',
- };
- throw e;
- }
-
- // Start deleting artist associations with the track.
- const deleteArtistsPromise: Promise =
- trx.delete()
- .from('artists_tracks')
- .where({ 'trackId': trackId });
-
- // Start deleting tag associations with the track.
- const deleteTagsPromise: Promise =
- trx.delete()
- .from('tracks_tags')
- .where({ 'trackId': trackId });
-
- // Start deleting the track.
- const deleteTrackPromise: Promise =
- trx.delete()
- .from('tracks')
- .where({ id: trackId });
-
- // Wait for the requests to finish.
- await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTrackPromise]);
- } catch (e) {
- trx.rollback();
+ // Start by retrieving the track itself for sanity.
+ const confirmTrackId: number | undefined =
+ await trx.select('id')
+ .from('tracks')
+ .where({ 'user': userId })
+ .where({ id: trackId })
+ .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined);
+
+ if (!confirmTrackId) {
+ const e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceNotFound,
+ message: 'Not all resources were found.',
+ };
throw e;
}
+
+ // Start deleting artist associations with the track.
+ const deleteArtistsPromise: Promise =
+ trx.delete()
+ .from('artists_tracks')
+ .where({ 'trackId': trackId });
+
+ // Start deleting tag associations with the track.
+ const deleteTagsPromise: Promise =
+ trx.delete()
+ .from('tracks_tags')
+ .where({ 'trackId': trackId });
+
+ // Start deleting the track.
+ const deleteTrackPromise: Promise =
+ trx.delete()
+ .from('tracks')
+ .where({ id: trackId });
+
+ // Wait for the requests to finish.
+ await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTrackPromise]);
})
}
\ No newline at end of file
diff --git a/server/db/User.ts b/server/db/User.ts
index 258489b..0590a7d 100644
--- a/server/db/User.ts
+++ b/server/db/User.ts
@@ -6,34 +6,30 @@ import { DBErrorKind, DBError } from '../endpoints/types';
export async function createUser(user: api.User, knex: Knex): Promise {
return await knex.transaction(async (trx) => {
- try {
- // check if the user already exists
- const newUser = (await trx
- .select('id')
- .from('users')
- .where({ email: user.email }))[0];
- if (newUser) {
- let e: DBError = {
- name: "DBError",
- kind: DBErrorKind.ResourceConflict,
- message: "User with given e-mail already exists.",
- }
- throw e;
+ // check if the user already exists
+ const newUser = (await trx
+ .select('id')
+ .from('users')
+ .where({ email: user.email }))[0];
+ if (newUser) {
+ let e: DBError = {
+ name: "DBError",
+ kind: DBErrorKind.ResourceConflict,
+ message: "User with given e-mail already exists.",
}
+ throw e;
+ }
- // Create the new user.
- const passwordHash = sha512(user.password);
- const userId = (await trx('users')
- .insert({
- email: user.email,
- passwordHash: passwordHash,
- })
- .returning('id') // Needed for Postgres
- )[0];
+ // Create the new user.
+ const passwordHash = sha512(user.password);
+ const userId = (await trx('users')
+ .insert({
+ email: user.email,
+ passwordHash: passwordHash,
+ })
+ .returning('id') // Needed for Postgres
+ )[0];
- return userId;
- } catch (e) {
- trx.rollback();
- }
+ return userId;
})
}
\ No newline at end of file
diff --git a/server/endpoints/Album.ts b/server/endpoints/Album.ts
index cdf1770..7ef0329 100644
--- a/server/endpoints/Album.ts
+++ b/server/endpoints/Album.ts
@@ -25,10 +25,10 @@ export const GetAlbum: EndpointHandler = async (req: any, res: any, knex: Knex)
}
export const PostAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPostAlbumRequest(req)) {
+ if (!api.checkPostAlbumRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
- message: 'Invalid PostAlbum request',
+ message: 'Invalid PostAlbum request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
@@ -40,14 +40,14 @@ export const PostAlbum: EndpointHandler = async (req: any, res: any, knex: Knex)
try {
let id = await createAlbum(userId, reqObject, knex);
- res.status(200).send(id);
+ res.status(200).send({ id: id });
} catch (e) {
handleErrorsInEndpoint(e);
}
}
export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPutAlbumRequest(req)) {
+ if (!api.checkPutAlbumRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PutAlbum request',
@@ -69,7 +69,7 @@ export const PutAlbum: EndpointHandler = async (req: any, res: any, knex: Knex)
}
export const PatchAlbum: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPatchAlbumRequest(req)) {
+ if (!api.checkPatchAlbumRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PatchAlbum request',
diff --git a/server/endpoints/Artist.ts b/server/endpoints/Artist.ts
index 1cdf455..1663ee2 100644
--- a/server/endpoints/Artist.ts
+++ b/server/endpoints/Artist.ts
@@ -16,10 +16,10 @@ export const GetArtist: EndpointHandler = async (req: any, res: any, knex: Knex)
}
export const PostArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPostArtistRequest(req)) {
+ if (!api.checkPostArtistRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
- message: 'Invalid PostArtist request',
+ message: 'Invalid PostArtist request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
@@ -40,7 +40,7 @@ export const PostArtist: EndpointHandler = async (req: any, res: any, knex: Knex
export const PutArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPutArtistRequest(req)) {
+ if (!api.checkPutArtistRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PutArtist request',
@@ -63,7 +63,7 @@ export const PutArtist: EndpointHandler = async (req: any, res: any, knex: Knex)
}
export const PatchArtist: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPatchArtistRequest(req)) {
+ if (!api.checkPatchArtistRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PatchArtist request',
diff --git a/server/endpoints/ImportExport.ts b/server/endpoints/ImportExport.ts
new file mode 100644
index 0000000..5b29868
--- /dev/null
+++ b/server/endpoints/ImportExport.ts
@@ -0,0 +1,45 @@
+import Knex from "knex";
+import { exportDB, importDB } from "../db/ImportExport";
+import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from "./types";
+import * as api from '../../client/src/api/api';
+import { DBExportEndpoint } from "../../client/src/api/api";
+
+export const DBExport: EndpointHandler = async (req: any, res: any, knex: Knex) => {
+ try {
+ let db = await exportDB(req.user.id, knex);
+ res.status(200);
+ res.setHeader('Content-disposition', 'attachment; filename= mudbase.json');
+ res.setHeader('Content-type', 'application/json');
+ res.send( JSON.stringify(db, null, 2) );
+ } catch (e) {
+ handleErrorsInEndpoint(e)
+ }
+}
+
+export const DBImport: EndpointHandler = async (req: any, res: any, knex: Knex) => {
+ if (!api.checkDBImportRequest(req.body)) {
+ const e: EndpointError = {
+ name: "EndpointError",
+ message: 'Invalid DBImport request',
+ httpStatus: 400
+ };
+ throw e;
+ }
+ const reqObject: api.DBImportRequest = req.body;
+ const { id: userId } = req.user;
+
+ console.log("User ", userId, ": Import DB ");
+
+ try {
+ await importDB(userId, reqObject, knex)
+ res.status(200).send();
+
+ } catch (e) {
+ handleErrorsInEndpoint(e);
+ }
+}
+
+export const importExportEndpoints: [ string, string, boolean, EndpointHandler ][] = [
+ [ api.DBExportEndpoint, 'get', true, DBExport ],
+ [ api.DBImportEndpoint, 'post', true, DBImport ],
+ ];
\ No newline at end of file
diff --git a/server/endpoints/Integration.ts b/server/endpoints/Integration.ts
index b872362..9be5be5 100644
--- a/server/endpoints/Integration.ts
+++ b/server/endpoints/Integration.ts
@@ -6,7 +6,7 @@ import { createIntegration, deleteIntegration, getIntegration, listIntegrations,
import { IntegrationDataWithId } from '../../client/src/api/api';
export const PostIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPostIntegrationRequest(req)) {
+ if (!api.checkPostIntegrationRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PostIntegration request',
@@ -67,7 +67,7 @@ export const DeleteIntegration: EndpointHandler = async (req: any, res: any, kne
}
export const PutIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPutIntegrationRequest(req)) {
+ if (!api.checkPutIntegrationRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PutIntegration request',
@@ -90,7 +90,7 @@ export const PutIntegration: EndpointHandler = async (req: any, res: any, knex:
}
export const PatchIntegration: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPatchIntegrationRequest(req)) {
+ if (!api.checkPatchIntegrationRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PatchIntegration request',
diff --git a/server/endpoints/Query.ts b/server/endpoints/Query.ts
index b0f3294..0e49515 100644
--- a/server/endpoints/Query.ts
+++ b/server/endpoints/Query.ts
@@ -18,7 +18,7 @@ export const Query: EndpointHandler = async (req: any, res: any, knex: Knex) =>
console.log("User ", userId, ": Query ", reqObject);
try {
- let r = doQuery(userId, reqObject, knex);
+ let r = await doQuery(userId, reqObject, knex);
res.status(200).send(r);
} catch (e) {
handleErrorsInEndpoint(e);
diff --git a/server/endpoints/Tag.ts b/server/endpoints/Tag.ts
index 6e82dab..8a1e06a 100644
--- a/server/endpoints/Tag.ts
+++ b/server/endpoints/Tag.ts
@@ -2,12 +2,13 @@ import * as api from '../../client/src/api/api';
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from './types';
import Knex from 'knex';
import { createTag, deleteTag, getTag, mergeTag, modifyTag } from '../db/Tag';
+import { getAllJSDocTagsOfKind } from 'typescript';
export const PostTag: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPostTagRequest(req)) {
+ if (!api.checkPostTagRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
- message: 'Invalid PostTag request',
+ message: 'Invalid PostTag request' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
@@ -55,7 +56,7 @@ export const GetTag: EndpointHandler = async (req: any, res: any, knex: Knex) =>
}
export const PutTag: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPutTagRequest(req)) {
+ if (!api.checkPutTagRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PutTag request',
@@ -77,7 +78,7 @@ export const PutTag: EndpointHandler = async (req: any, res: any, knex: Knex) =>
}
export const PatchTag: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPatchTagRequest(req)) {
+ if (!api.checkPatchTagRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PatchTag request',
diff --git a/server/endpoints/Track.ts b/server/endpoints/Track.ts
index cc649aa..c9a8a6e 100644
--- a/server/endpoints/Track.ts
+++ b/server/endpoints/Track.ts
@@ -5,7 +5,7 @@ import asJson from '../lib/asJson';
import { createTrack, deleteTrack, getTrack, modifyTrack } from '../db/Track';
export const PostTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPostTrackRequest(req)) {
+ if (!api.checkPostTrackRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PostTrack request',
@@ -19,8 +19,9 @@ export const PostTrack: EndpointHandler = async (req: any, res: any, knex: Knex)
console.log("User ", userId, ": Post Track ", reqObject);
try {
+ let id = await createTrack(userId, reqObject, knex);
res.status(200).send({
- id: await createTrack(userId, reqObject, knex)
+ id: id,
});
} catch (e) {
@@ -40,7 +41,7 @@ export const GetTrack: EndpointHandler = async (req: any, res: any, knex: Knex)
}
export const PutTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPutTrackRequest(req)) {
+ if (!api.checkPutTrackRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PutTrack request',
@@ -62,7 +63,7 @@ export const PutTrack: EndpointHandler = async (req: any, res: any, knex: Knex)
}
export const PatchTrack: EndpointHandler = async (req: any, res: any, knex: Knex) => {
- if (!api.checkPatchTrackRequest(req)) {
+ if (!api.checkPatchTrackRequest(req.body)) {
const e: EndpointError = {
name: "EndpointError",
message: 'Invalid PatchTrack request',
diff --git a/server/migrations/20200828124218_init_db.ts b/server/migrations/20200828124218_init_db.ts
index 70fc0ab..6cc8d24 100644
--- a/server/migrations/20200828124218_init_db.ts
+++ b/server/migrations/20200828124218_init_db.ts
@@ -9,8 +9,8 @@ export async function up(knex: Knex): Promise {
table.increments('id');
table.string('name');
table.string('storeLinks')
- table.integer('user').unsigned().notNullable().defaultTo(1);
- table.integer('album').unsigned().defaultTo(null);
+ table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
+ table.integer('album').unsigned().defaultTo(null).references('albums.id');
}
)
@@ -21,7 +21,7 @@ export async function up(knex: Knex): Promise {
table.increments('id');
table.string('name');
table.string('storeLinks');
- table.integer('user').unsigned().notNullable().defaultTo(1);
+ table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
}
)
@@ -32,7 +32,7 @@ export async function up(knex: Knex): Promise {
table.increments('id');
table.string('name');
table.string('storeLinks');
- table.integer('user').unsigned().notNullable().defaultTo(1);
+ table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
}
)
@@ -43,7 +43,7 @@ export async function up(knex: Knex): Promise {
table.increments('id');
table.string('name');
table.integer('parentId');
- table.integer('user').unsigned().notNullable().defaultTo(1);
+ table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
}
)
@@ -62,7 +62,7 @@ export async function up(knex: Knex): Promise {
'integrations',
(table: any) => {
table.increments('id');
- table.integer('user').unsigned().notNullable().defaultTo(1);
+ table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
table.string('name').notNullable(); // Uniquely identifies this integration configuration for the user.
table.string('type').notNullable(); // Enumerates different supported integration types (e.g. Spotify)
table.string('details'); // Stores anything that might be needed for the integration to work.
@@ -76,8 +76,8 @@ export async function up(knex: Knex): Promise {
'tracks_artists',
(table: any) => {
table.increments('id');
- table.integer('trackId');
- table.integer('artistId');
+ table.integer('trackId').references('tracks.id');
+ table.integer('artistId').references('artists.id');
table.unique(['trackId', 'artistId'])
}
)
@@ -87,8 +87,8 @@ export async function up(knex: Knex): Promise {
'tracks_tags',
(table: any) => {
table.increments('id');
- table.integer('trackId');
- table.integer('tagId');
+ table.integer('trackId').references('tracks.id');
+ table.integer('tagId').references('tags.id');
table.unique(['trackId', 'tagId'])
}
)
@@ -98,8 +98,8 @@ export async function up(knex: Knex): Promise {
'artists_tags',
(table: any) => {
table.increments('id');
- table.integer('artistId');
- table.integer('tagId');
+ table.integer('artistId').references('artists.id');
+ table.integer('tagId').references('tags.id');
table.unique(['artistId', 'tagId'])
}
)
@@ -109,8 +109,8 @@ export async function up(knex: Knex): Promise {
'albums_tags',
(table: any) => {
table.increments('id');
- table.integer('tagId');
- table.integer('albumId');
+ table.integer('tagId').references('tags.id');
+ table.integer('albumId').references('albums.id');
table.unique(['albumId', 'tagId'])
}
)
@@ -120,8 +120,8 @@ export async function up(knex: Knex): Promise {
'artists_albums',
(table: any) => {
table.increments('id');
- table.integer('artistId');
- table.integer('albumId');
+ table.integer('artistId').references('artists.id');
+ table.integer('albumId').references('albums.id');
table.unique(['artistId', 'albumId'])
}
)
diff --git a/server/package.json b/server/package.json
index a4d50b9..6bd9809 100644
--- a/server/package.json
+++ b/server/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"scripts": {
"start": "ts-node server.ts",
- "dev": "nodemon server.ts",
+ "dev": "API='/api' nodemon server.ts",
"build": "tsc",
"test": "ts-node node_modules/jasmine/bin/jasmine --config=test/jasmine.json"
},