A lot of stuff working again. Added export button.

editsong
Sander Vocke 5 years ago
parent bb9a1bdfa6
commit 35cd904a63
  1. 74
      client/package-lock.json
  2. 6
      client/package.json
  3. 1
      client/src/api/api.ts
  4. 44
      client/src/api/endpoints/importexport.ts
  5. 14
      client/src/api/types/resources.ts
  6. 9
      client/src/components/MainWindow.tsx
  7. 21
      client/src/components/tables/ResultsTable.tsx
  8. 10
      client/src/components/windows/manage/ManageWindow.tsx
  9. 65
      client/src/components/windows/manage_importexport/ManageImportExport.tsx
  10. 2
      client/src/components/windows/manage_links/BatchLinkDialog.tsx
  11. 2
      client/src/components/windows/query/QueryWindow.tsx
  12. 6
      client/src/lib/trackGetters.tsx
  13. 11
      client/src/setupProxy.js
  14. 105
      scripts/gpm_retrieve/gpm_retrieve.py
  15. 2
      server/app.ts
  16. 583
      server/db/Album.ts
  17. 541
      server/db/Artist.ts
  18. 82
      server/db/ImportExport.ts
  19. 124
      server/db/Integration.ts
  20. 75
      server/db/Query.ts
  21. 339
      server/db/Tag.ts
  22. 468
      server/db/Track.ts
  23. 48
      server/db/User.ts
  24. 10
      server/endpoints/Album.ts
  25. 8
      server/endpoints/Artist.ts
  26. 45
      server/endpoints/ImportExport.ts
  27. 6
      server/endpoints/Integration.ts
  28. 2
      server/endpoints/Query.ts
  29. 9
      server/endpoints/Tag.ts
  30. 9
      server/endpoints/Track.ts
  31. 32
      server/migrations/20200828124218_init_db.ts
  32. 2
      server/package.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",

@ -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/"
}
}

@ -9,5 +9,6 @@
export * from './types/resources';
export * from './endpoints/auth';
export * from './endpoints/importexport';
export * from './endpoints/resources';
export * from './endpoints/query';

@ -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);
}

@ -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;
}

@ -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) {
<AppBar selectedTab={AppBarTab.Browse} />
<AlbumWindow />
</PrivateRoute>
<PrivateRoute path="/song/:id">
<PrivateRoute path="/track/:id">
<AppBar selectedTab={AppBarTab.Browse} />
<SongWindow />
<TrackWindow />
</PrivateRoute>
<PrivateRoute path="/manage/tags">
<AppBar selectedTab={AppBarTab.Manage} />
@ -96,6 +97,10 @@ export default function MainWindow(props: any) {
<AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.Links} />
</PrivateRoute>
<PrivateRoute path="/manage/importexport">
<AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.ImportExport} />
</PrivateRoute>
<PrivateRoute exact path="/manage">
<Redirect to={"/manage/tags"} />
</PrivateRoute>

@ -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: {
</TableHead>
<TableBody>
{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: {
</TableCell>;
}
return <TableRow key={title}>
<TextCell align="left" _onClick={onClickTrack}>{title}</TextCell>
return <TableRow key={name}>
<TextCell align="left" _onClick={onClickTrack}>{name}</TextCell>
<TextCell align="left" _onClick={onClickArtist}>{artist}</TextCell>
<TextCell align="left" _onClick={onClickAlbum}>{album}</TextCell>
{album ? <TextCell align="left" _onClick={onClickAlbum}>{album}</TextCell> : <TextCell/>}
<TableCell padding="none" align="left" width="25%">
<Box display="flex" alignItems="center">
{tags}

@ -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')}
/>
<NavButton
label="Import/Export"
icon={<SaveIcon />}
selected={props.selectedWindow === ManageWhat.ImportExport}
onClick={() => history.push('/manage/importexport')}
/>
</Box>
{props.selectedWindow === ManageWhat.Tags && <ManageTagsWindow/>}
{props.selectedWindow === ManageWhat.Links && <ManageLinksWindow/>}
{props.selectedWindow === ManageWhat.ImportExport && <ManageImportExportWindow/>}
</Box >
}

@ -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 <ManageImportExportWindowControlled state={state} dispatch={dispatch} />
}
export function ManageImportExportWindowControlled(props: {
state: ManageImportExportWindowState,
dispatch: (action: any) => void,
}) {
return <>
<Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="80%"
>
<SaveIcon style={{ fontSize: 80 }} />
</Box>
<Box
m={1}
mt={4}
width="80%"
>
<Typography variant="h4">Import / Export</Typography>
<Box mt={2} />
<Typography>
An exported database contains all your artists, albums, tracks and tags.<br />
It is represented as a JSON structure.
</Typography>
<Box mt={2} />
<Alert severity="warning">Upon importing a previously exported database, your database will be completely replaced!</Alert>
<Box mt={2} />
<a href={(process.env.REACT_APP_BACKEND || "") + serverApi.DBExportEndpoint}>
<Button variant="outlined">Export</Button>
</a>
</Box>
</Box>
</>
}

@ -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}` +

@ -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<any> {

@ -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) => {

@ -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,
})
);
};

@ -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)

@ -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;

@ -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<number> {
return await knex.transaction(async (trx) => {
try {
// Start retrieving artists.
const artistIdsPromise: Promise<number[]> =
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<number[]> =
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<number[]> =
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<number[]> =
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<number[]> =
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<number[]> =
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<void> {
await knex.transaction(async (trx) => {
try {
// Start retrieving the album itself.
const albumIdPromise: Promise<number | undefined> =
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<number[] | undefined> =
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<number[] | undefined> =
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<number | undefined> =
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<number[] | undefined> =
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<number[] | undefined> =
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<void> {
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<any> =
trx.delete()
.from('artists_albums')
.where({ 'albumId': albumId });
// Start deleting tag associations with the album.
const deleteTagsPromise: Promise<any> =
trx.delete()
.from('albums_tags')
.where({ 'albumId': albumId });
// Start deleting track associations with the album.
const deleteTracksPromise: Promise<any> =
trx.delete()
.from('tracks_albums')
.where({ 'albumId': albumId });
// Start deleting the album.
const deleteAlbumPromise: Promise<any> =
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<any> =
trx.delete()
.from('artists_albums')
.where({ 'albumId': albumId });
// Start deleting tag associations with the album.
const deleteTagsPromise: Promise<any> =
trx.delete()
.from('albums_tags')
.where({ 'albumId': albumId });
// Start deleting track associations with the album.
const deleteTracksPromise: Promise<any> =
trx.delete()
.from('tracks_albums')
.where({ 'albumId': albumId });
// Start deleting the album.
const deleteAlbumPromise: Promise<any> =
trx.delete()
.from('albums')
.where({ id: albumId });
// Wait for the requests to finish.
await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTracksPromise, deleteAlbumPromise]);
})
}

@ -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<api.TrackWithId[]> =
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<number> {
return await knex.transaction(async (trx) => {
try {
// Start retrieving albums.
const albumIdsPromise: Promise<number[]> =
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<number[]> =
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<number[]> =
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<number[]> =
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<number[]> =
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<void> {
await knex.transaction(async (trx) => {
try {
// Start retrieving the artist itself.
const artistIdPromise: Promise<number | undefined> =
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<number[] | undefined> =
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<number | undefined> =
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<number[] | undefined> =
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<number[] | undefined> =
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<void> {
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<any> =
trx.delete()
.from('artists_albums')
.where({ 'artistId': artistId });
// Start deleting tag associations with the artist.
const deleteTagsPromise: Promise<any> =
trx.delete()
.from('artists_tags')
.where({ 'artistId': artistId });
// Start deleting track associations with the artist.
const deleteTracksPromise: Promise<any> =
trx.delete()
.from('tracks_artists')
.where({ 'artistId': artistId });
// Start deleting the artist.
const deleteArtistPromise: Promise<any> =
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<any> =
trx.delete()
.from('artists_albums')
.where({ 'artistId': artistId });
// Start deleting tag associations with the artist.
const deleteTagsPromise: Promise<any> =
trx.delete()
.from('artists_tags')
.where({ 'artistId': artistId });
// Start deleting track associations with the artist.
const deleteTracksPromise: Promise<any> =
trx.delete()
.from('tracks_artists')
.where({ 'artistId': artistId });
// Start deleting the artist.
const deleteArtistPromise: Promise<any> =
trx.delete()
.from('artists')
.where({ id: artistId });
// Wait for the requests to finish.
await Promise.all([deleteAlbumsPromise, deleteTagsPromise, deleteTracksPromise, deleteArtistPromise]);
})
}

@ -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<DBImportExportFormat> {
export async function exportDB(userId: number, knex: Knex): Promise<api.DBImportExportFormat> {
// First, retrieve all the objects without taking linking tables into account.
// Fetch the links separately.
let tracksPromise: Promise<api.TrackWithRefsWithId[]> =
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<DBImportExpo
name: t.name,
id: t.id,
storeLinks: asJson(t.storeLinks),
albumId: t.albumId,
albumId: t.album,
artistIds: [],
tagIds: [],
}
@ -137,6 +123,10 @@ export async function exportDB(userId: number, knex: Knex): Promise<DBImportExpo
tracksArtists.forEach((v: [number, number]) => {
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<DBImportExpo
}
}
export async function importDB(userId: number, db: DBImportExportFormat, knex: Knex): Promise<void> {
export async function importDB(userId: number, db: api.DBImportExportFormat, knex: Knex): Promise<void> {
return await knex.transaction(async (trx) => {
// Store the ID mappings in this record.
let tagIdMaps: Record<number, number> = {};
let artistIdMaps: Record<number, number> = {};
let albumIdMaps: Record<number, number> = {};
let trackIdMaps: Record<number, number> = {};
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);
}))
});
}

@ -6,25 +6,20 @@ import { IntegrationDataWithId, IntegrationDataWithSecret, PartialIntegrationDat
export async function createIntegration(userId: number, integration: api.IntegrationDataWithSecret, knex: Knex): Promise<number> {
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<api.
export async function deleteIntegration(userId: number, id: number, knex: Knex) {
await knex.transaction(async (trx) => {
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<void> {
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)
})
}

@ -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 <api.Track>{
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<ObjectType, string> = {
// 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<ObjectType> {
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<number, any[]> = {};
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;
}

@ -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<number> {
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<any> =
trx.delete()
.from('artists_tags')
.whereIn('tagId', toDelete);
// Start deleting album associations with the tag.
const deleteAlbumsPromise: Promise<any> =
trx.delete()
.from('albums_tags')
.whereIn('tagId', toDelete);
// Start deleting track associations with the tag.
const deleteTracksPromise: Promise<any> =
trx.delete()
.from('tracks_tags')
.whereIn('tagId', toDelete);
// Start deleting the tag and its children.
const deleteTags: Promise<any> = 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<any> =
trx.delete()
.from('artists_tags')
.whereIn('tagId', toDelete);
// Start deleting album associations with the tag.
const deleteAlbumsPromise: Promise<any> =
trx.delete()
.from('albums_tags')
.whereIn('tagId', toDelete);
// Start deleting track associations with the tag.
const deleteTracksPromise: Promise<any> =
trx.delete()
.from('tracks_tags')
.whereIn('tagId', toDelete);
// Start deleting the tag and its children.
const deleteTags: Promise<any> = 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<void> {
await knex.transaction(async (trx) => {
try {
// Start retrieving the parent tag.
const parentTagIdPromise: Promise<number | undefined | null> = 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<number | undefined | null> = 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<void> {
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();
})
}

@ -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<number> {
return await knex.transaction(async (trx) => {
try {
// Start retrieving artists.
const artistIdsPromise: Promise<number[]> =
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<number[]> =
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<number[]> =
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<number[]> =
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<number | null> =
knex.select('id')
// Start retrieving album.
const albumIdPromise: Promise<number | null> =
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<void> {
await knex.transaction(async (trx) => {
try {
// Start retrieving the track itself.
const trackIdPromise: Promise<number | undefined> =
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<number[] | undefined> =
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<number | undefined> =
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<number[] | undefined> =
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<void> {
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<any> =
trx.delete()
.from('artists_tracks')
.where({ 'trackId': trackId });
// Start deleting tag associations with the track.
const deleteTagsPromise: Promise<any> =
trx.delete()
.from('tracks_tags')
.where({ 'trackId': trackId });
// Start deleting the track.
const deleteTrackPromise: Promise<any> =
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<any> =
trx.delete()
.from('artists_tracks')
.where({ 'trackId': trackId });
// Start deleting tag associations with the track.
const deleteTagsPromise: Promise<any> =
trx.delete()
.from('tracks_tags')
.where({ 'trackId': trackId });
// Start deleting the track.
const deleteTrackPromise: Promise<any> =
trx.delete()
.from('tracks')
.where({ id: trackId });
// Wait for the requests to finish.
await Promise.all([deleteArtistsPromise, deleteTagsPromise, deleteTrackPromise]);
})
}

@ -6,34 +6,30 @@ import { DBErrorKind, DBError } from '../endpoints/types';
export async function createUser(user: api.User, knex: Knex): Promise<number> {
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;
})
}

@ -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',

@ -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',

@ -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 ],
];

@ -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',

@ -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);

@ -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',

@ -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',

@ -9,8 +9,8 @@ export async function up(knex: Knex): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
'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<void> {
'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<void> {
'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<void> {
'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<void> {
'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<void> {
'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'])
}
)

@ -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"
},

Loading…
Cancel
Save