diff --git a/client/src/App.tsx b/client/src/App.tsx
index 8825d6f..0a9bf6e 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -7,6 +7,8 @@ import * as serverApi from './api';
import AppBar, { ActiveTab as AppBarActiveTab } from './components/AppBar';
import ItemList from './components/ItemList';
import ItemListItem from './components/ItemListItem';
+import FilterControl from './components/FilterControl';
+import { SongQuery, toApiQuery } from './types/Query';
import { SongDisplayItem, ArtistDisplayItem } from './types/DisplayItem';
import { ReactComponent as GooglePlayIcon } from './assets/googleplaymusic_icon.svg';
@@ -17,9 +19,10 @@ import {
useHistory,
Redirect
} from "react-router-dom";
+import { timeLog } from 'console';
interface SongItemProps {
- id: Number,
+ song: serverApi.SongDetails,
}
interface ArtistItemProps {
@@ -28,70 +31,41 @@ interface ArtistItemProps {
const getStoreIcon = (url: String) => {
if (url.includes('play.google.com')) {
- return ;
+ return ;
}
- return ;
+ return ;
}
function SongItem(props: SongItemProps) {
- const [songDisplayItem, setSongDisplayItem] = React.useState(undefined);
-
- const updateSong = async () => {
- const response: any = await fetch(serverApi.SongDetailsEndpoint.replace(':id', props.id.toString()));
- const json: any = await response.json();
- const title: String | undefined = json.title;
- const artistIds: Number[] | undefined = json.artistIds;
- const artistNamesPromises: Promise[] | undefined = artistIds && artistIds.map((id: Number) => {
- return fetch(serverApi.ArtistDetailsEndpoint.replace(':id', id.toString()))
- .then((response: any) => response.json())
- .then((json: any) => json.name);
- });
- const artistNames: String[] | undefined = artistNamesPromises && await Promise.all(artistNamesPromises);
-
- return {
- title: title ? title : "Unknown",
- artistNames: artistNames ? artistNames : [],
- storeLinks: json.storeLinks.map((url: String) => {
- return {
- icon: getStoreIcon(url),
- url: url
- }
- }),
- };
- };
- useEffect(() => {
- updateSong().then((song: SongDisplayItem) => { setSongDisplayItem(song); });
- }, []);
+ const displayItem: SongDisplayItem = {
+ title: props.song.title,
+ artistNames: props.song.artists && props.song.artists.map((artist: serverApi.ArtistDetails) => {
+ return artist.name;
+ }) || ['Unknown'],
+ tagNames: props.song.tags && props.song.tags.map((tag: serverApi.TagDetails) => {
+ return tag.name;
+ }) || [],
+ storeLinks: []
+ // json.storeLinks.map((url: String) => {
+ // return {
+ // icon: getStoreIcon(url),
+ // url: url
+ // }
+ // })
+ }
- return ;
+ return ;
}
-function SongList() {
- const [songs, setSongs] = useState([]);
-
- React.useEffect(() => {
- const request: serverApi.QuerySongsRequest = {
- query: {}
- }
- const requestOpts = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(request)
- };
- fetch(serverApi.QuerySongsEndpoint, requestOpts)
- .then((response: any) => response.json())
- .then((json: any) => {
- 'ids' in json && setSongs(json.ids);
- });
- }, []);
-
+interface SongListProps {
+ songs: serverApi.SongDetails[]
+}
+function SongList(props: SongListProps) {
return
- {songs.map((song: any) => {
- return ;
+ {props.songs.map((song: any) => {
+ return ;
})}
;
@@ -103,9 +77,17 @@ function ArtistItem(props: ArtistItemProps) {
const updateArtist = async () => {
const response: any = await fetch(serverApi.ArtistDetailsEndpoint.replace(':id', props.id.toString()));
const json: any = await response.json();
+ const tagIds: Number[] | undefined = json.tagIds;
+ const tagNamesPromises: Promise[] | undefined = tagIds && tagIds.map((id: Number) => {
+ return fetch(serverApi.TagDetailsEndpoint.replace(':id', id.toString()))
+ .then((response: any) => response.json())
+ .then((json: any) => json.name);
+ });
+ const tagNames: String[] | undefined = tagNamesPromises && await Promise.all(tagNamesPromises);
return {
name: json.name ? json.name : "Unknown",
+ tagNames: tagNames ? tagNames : [],
storeLinks: json.storeLinks.map((url: String) => {
return {
icon: getStoreIcon(url),
@@ -129,7 +111,8 @@ function ArtistList() {
React.useEffect(() => {
const request: serverApi.QueryArtistsRequest = {
- query: {}
+ offset: 0,
+ limit: 20,
}
const requestOpts = {
method: 'POST',
@@ -154,6 +137,30 @@ function ArtistList() {
function AppBody() {
const history = useHistory();
+ const [songQuery, setSongQuery] = useState({
+ 'titleLike': ''
+ });
+ const [songs, setSongs] = useState([]);
+
+ React.useEffect(() => {
+ const query = songQuery;
+ setSongs([]);
+ const request: serverApi.QuerySongsRequest = {
+ query: toApiQuery(query),
+ offset: 0,
+ limit: 20,
+ }
+ const requestOpts = {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(request)
+ };
+ fetch(serverApi.QuerySongsEndpoint, requestOpts)
+ .then((response: any) => response.json())
+ .then((json: any) => {
+ 'songs' in json && query === songQuery && setSongs(json.songs);
+ });
+ }, [songQuery]);
const onAppBarTabChange = (value: AppBarActiveTab) => {
switch (value) {
@@ -174,8 +181,12 @@ function AppBody() {
+ { setSongQuery(query); }}
+ />
-
+
diff --git a/client/src/api.ts b/client/src/api.ts
index ebbc92e..f0d31c4 100644
--- a/client/src/api.ts
+++ b/client/src/api.ts
@@ -7,6 +7,22 @@
// a request structure, a response structure and
// a checking function which determines request validity.
+export interface ArtistDetails {
+ id: Number,
+ name: String,
+}
+export interface TagDetails {
+ id: Number,
+ name: String,
+ parent?: TagDetails,
+}
+export interface SongDetails {
+ id: Number,
+ title: String,
+ artists?: ArtistDetails[],
+ tags?: TagDetails[],
+}
+
// Query for songs (POST).
export const QuerySongsEndpoint = '/song/query';
export enum SongQueryElemOp {
@@ -17,9 +33,11 @@ export enum SongQueryFilterOp {
Eq = "EQ",
Ne = "NE",
In = "IN",
- NotIn = "NOTIN"
+ NotIn = "NOTIN",
+ Like = "LIKE",
}
export enum SongQueryElemProperty {
+ title = "title",
id = "id",
artistIds = "artistIds",
albumIds = "albumIds",
@@ -33,10 +51,12 @@ export interface SongQueryElem {
}
export interface SongQuery extends SongQueryElem { }
export interface QuerySongsRequest {
- query: SongQuery
+ query: SongQuery,
+ offset: Number,
+ limit: Number,
}
export interface QuerySongsResponse {
- ids: Number[]
+ songs: SongDetails[]
}
export function checkQuerySongsElem(elem: any): boolean {
if (elem.childrenOperator && elem.children) {
@@ -51,7 +71,10 @@ export function checkQuerySongsElem(elem: any): boolean {
Object.keys(elem).length == 0;
}
export function checkQuerySongsRequest(req: any): boolean {
- return "query" in req && checkQuerySongsElem(req.query);
+ return 'query' in req
+ && 'offset' in req
+ && 'limit' in req
+ && checkQuerySongsElem(req.query);
}
// Get song details (GET).
@@ -70,12 +93,16 @@ export function checkSongDetailsRequest(req: any): boolean {
// Query for artists.
export const QueryArtistsEndpoint = '/artist/query';
-export interface QueryArtistsRequest { }
+export interface QueryArtistsRequest {
+ offset: Number,
+ limit: Number,
+}
export interface QueryArtistsResponse {
ids: Number[]
}
export function checkQueryArtistsRequest(req: any): boolean {
- return true;
+ return 'offset' in req
+ && 'limit' in req;
}
// Get artist details (GET).
diff --git a/client/src/components/FilterControl.tsx b/client/src/components/FilterControl.tsx
new file mode 100644
index 0000000..92e8e7c
--- /dev/null
+++ b/client/src/components/FilterControl.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+
+import {
+ TextField,
+ Paper,
+ Select,
+ MenuItem,
+ Typography
+} from '@material-ui/core';
+
+import {
+ TitleQuery,
+ ArtistQuery,
+ isTitleQuery,
+ isArtistQuery,
+ SongQuery
+} from '../types/Query';
+
+
+interface TitleFilterControlProps {
+ query: TitleQuery,
+ onChangeQuery: (q: SongQuery) => void,
+}
+function TitleFilterControl(props: TitleFilterControlProps) {
+ return props.onChangeQuery({
+ titleLike: i.target.value
+ })}
+ />
+}
+
+interface ArtistFilterControlProps {
+ query: ArtistQuery,
+ onChangeQuery: (q: SongQuery) => void,
+}
+function ArtistFilterControl(props: ArtistFilterControlProps) {
+ return props.onChangeQuery({
+ artistLike: i.target.value
+ })}
+ />
+}
+
+export interface IProps {
+ query: SongQuery,
+ onChangeQuery: (query: SongQuery) => void,
+}
+
+export default function FilterControl(props: IProps) {
+ const selectOptions: string[] = ['Title', 'Artist'];
+ const selectOption: string = (isTitleQuery(props.query) && 'Title') ||
+ (isArtistQuery(props.query) && 'Artist') ||
+ "Unknown";
+
+ const handleQueryOnChange = (event: any) => {
+ switch (event.target.value) {
+ case 'Title': {
+ props.onChangeQuery({
+ titleLike: ''
+ })
+ break;
+ }
+ case 'Artist': {
+ props.onChangeQuery({
+ artistLike: ''
+ })
+ break;
+ }
+ }
+ }
+
+ return
+
+ {isTitleQuery(props.query) && }
+ {isArtistQuery(props.query) && }
+ ;
+}
\ No newline at end of file
diff --git a/client/src/components/ItemListLoadedArtistItem.tsx b/client/src/components/ItemListLoadedArtistItem.tsx
index 4d4c7a9..4120147 100644
--- a/client/src/components/ItemListLoadedArtistItem.tsx
+++ b/client/src/components/ItemListLoadedArtistItem.tsx
@@ -3,6 +3,7 @@ import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import GroupIcon from '@material-ui/icons/Group';
+import Chip from '@material-ui/core/Chip';
import { ArtistDisplayItem } from '../types/DisplayItem';
@@ -19,6 +20,9 @@ export default function ItemListLoadedArtistItem(props: IProps) {
+ {props.item.tagNames.map((tag: any) => {
+ return
+ })}
{props.item.storeLinks.map((link: any) => {
return
diff --git a/client/src/components/ItemListLoadedSongItem.tsx b/client/src/components/ItemListLoadedSongItem.tsx
index 0fb5330..f2210b4 100644
--- a/client/src/components/ItemListLoadedSongItem.tsx
+++ b/client/src/components/ItemListLoadedSongItem.tsx
@@ -3,6 +3,7 @@ import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import MusicNoteIcon from '@material-ui/icons/MusicNote';
+import Chip from '@material-ui/core/Chip';
import { SongDisplayItem } from '../types/DisplayItem';
@@ -25,6 +26,9 @@ export default function ItemListLoadedSongItem(props: IProps) {
primary={props.item.title}
secondary={artists}
/>
+ {props.item.tagNames.map((tag: any) => {
+ return
+ })}
{props.item.storeLinks.map((link: any) => {
return
diff --git a/client/src/types/DisplayItem.tsx b/client/src/types/DisplayItem.tsx
index b7dd1f2..bfa76d0 100644
--- a/client/src/types/DisplayItem.tsx
+++ b/client/src/types/DisplayItem.tsx
@@ -1,6 +1,7 @@
export interface SongDisplayItem {
title:String,
artistNames:String[],
+ tagNames:String[],
storeLinks: {
icon: JSX.Element,
url: String,
@@ -13,6 +14,7 @@ export interface LoadingSongDisplayItem {
export interface ArtistDisplayItem {
name:String,
+ tagNames:String[],
storeLinks: {
icon: JSX.Element,
url: String,
diff --git a/client/src/types/Query.tsx b/client/src/types/Query.tsx
new file mode 100644
index 0000000..4396088
--- /dev/null
+++ b/client/src/types/Query.tsx
@@ -0,0 +1,32 @@
+import { SongQueryElemProperty, SongQueryFilterOp } from '../api';
+
+export interface TitleQuery {
+ titleLike: String
+};
+export function isTitleQuery(q: SongQuery): q is TitleQuery {
+ return "titleLike" in q;
+}
+export function TitleToApiQuery(q: TitleQuery) {
+ return {
+ 'prop': SongQueryElemProperty.title,
+ 'propOperand': '%' + q.titleLike + '%',
+ 'propOperator': SongQueryFilterOp.Like,
+ }
+}
+
+export interface ArtistQuery {
+ artistLike: String
+};
+export function isArtistQuery(q: SongQuery): q is ArtistQuery {
+ return "artistLike" in q;
+}
+export function ArtistToApiQuery(q: ArtistQuery) {
+ return {
+ }
+}
+
+export type SongQuery = TitleQuery | ArtistQuery;
+export function toApiQuery(q: SongQuery) {
+ return (isTitleQuery(q) && TitleToApiQuery(q)) ||
+ (isArtistQuery(q) && ArtistToApiQuery(q)) || {};
+}
\ No newline at end of file
diff --git a/scripts/gpm_retrieve/gpm_retrieve.py b/scripts/gpm_retrieve/gpm_retrieve.py
index fba8965..4ca7aa4 100755
--- a/scripts/gpm_retrieve/gpm_retrieve.py
+++ b/scripts/gpm_retrieve/gpm_retrieve.py
@@ -35,6 +35,12 @@ def transferLibrary(gpm_api, mudbase_api):
return [];
artistStoreIds = [ [ getArtistStoreIds(song) for song in songs if song['artist'] == artist ][0] for artist in artists ]
+ # Create GPM import tag
+ gpmTagIdResponse = requests.post(mudbase_api + '/tag', data = {
+ 'name': 'GPM Import'
+ }).json()
+ print(f"Created tag \"GPM Import\", response: {gpmTagIdResponse}")
+
# Create genres and store their mudbase Ids
genreRootResponse = requests.post(mudbase_api + '/tag', data = {
'name': 'Genre'
@@ -52,9 +58,10 @@ def transferLibrary(gpm_api, mudbase_api):
# Create artists and store their mudbase Ids
artistMudbaseIds = []
for idx,artist in enumerate(artists):
- response = requests.post(mudbase_api + '/artist', data = {
+ response = requests.post(mudbase_api + '/artist', json = {
'name': artist,
- 'storeLinks': [ 'https://play.google.com/music/m/' + id for id in artistStoreIds[idx] ]
+ 'storeLinks': [ 'https://play.google.com/music/m/' + id for id in artistStoreIds[idx] ],
+ 'tagIds': [ gpmTagIdResponse['id'] ]
}).json()
print(f"Created artist \"{artist}\", response: {response}")
artistMudbaseIds.append(response['id'])
@@ -70,7 +77,7 @@ def transferLibrary(gpm_api, mudbase_api):
response = requests.post(mudbase_api + '/song', json = {
'title': song['title'],
'artistIds': [ artistMudbaseId ],
- 'tagIds' : [ genreMudbaseId ],
+ 'tagIds' : [ genreMudbaseId, gpmTagIdResponse['id'] ],
'storeLinks': [ 'https://play.google.com/music/m/' + id for id in getSongStoreIds(song) ],
}).json()
print(f"Created song \"{song['title']}\" with artist ID {artistMudbaseId}, response: {response}")
diff --git a/server/endpoints/CreateArtistEndpointHandler.ts b/server/endpoints/CreateArtistEndpointHandler.ts
index 8d02ac8..22fb5ed 100644
--- a/server/endpoints/CreateArtistEndpointHandler.ts
+++ b/server/endpoints/CreateArtistEndpointHandler.ts
@@ -13,40 +13,40 @@ export const CreateArtistEndpointHandler: EndpointHandler = async (req: any, res
}
const reqObject: api.CreateArtistRequest = req.body;
- // Start retrieving the tag instances to link the artist to.
- var tagInstancesPromise = reqObject.tagIds && models.Tag.findAll({
- where: {
- id: {
- [Op.in]: reqObject.tagIds
- }
- }
- });
+ console.log("Create artist:", reqObject)
- // Upon finish retrieving artists and albums, create the artist and associate it.
- await Promise.all([tagInstancesPromise])
- .then((values: any) => {
- var [tags] = values;
+ try {
- if (reqObject.tagIds && tags.length !== reqObject.tagIds.length) {
- const e: EndpointError = {
- internalMessage: 'Not all atags exist for CreateArtist request: ' + JSON.stringify(req.body),
- httpStatus: 400
- };
- throw e;
+ // Start retrieving the tag instances to link the artist to.
+ const tags = reqObject.tagIds && await models.Tag.findAll({
+ where: {
+ id: {
+ [Op.in]: reqObject.tagIds
+ }
}
+ });
- var artist = models.Artist.build({
- name: reqObject.name,
- storeLinks: reqObject.storeLinks || [],
- });
- tags && artist.addTags(tags);
- return artist.save();
- })
- .then((artist: any) => {
- const responseObject: api.CreateSongResponse = {
- id: artist.id
+ console.log("Found artist tags:", tags)
+
+ if (reqObject.tagIds && tags.length !== reqObject.tagIds.length) {
+ const e: EndpointError = {
+ internalMessage: 'Not all tags exist for CreateArtist request: ' + JSON.stringify(req.body),
+ httpStatus: 400
};
- res.status(200).send(responseObject);
- })
- .catch(catchUnhandledErrors);
+ throw e;
+ }
+
+ var artist = models.Artist.build({
+ name: reqObject.name,
+ storeLinks: reqObject.storeLinks || [],
+ });
+ tags && artist.addTags(tags);
+ await artist.save();
+ const responseObject: api.CreateSongResponse = {
+ id: artist.id
+ };
+ await res.status(200).send(responseObject);
+ } catch (e) {
+ catchUnhandledErrors(e);
+ }
}
\ No newline at end of file
diff --git a/server/endpoints/QueryArtistsEndpointHandler.ts b/server/endpoints/QueryArtistsEndpointHandler.ts
index 30788bd..d521935 100644
--- a/server/endpoints/QueryArtistsEndpointHandler.ts
+++ b/server/endpoints/QueryArtistsEndpointHandler.ts
@@ -3,14 +3,19 @@ import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
export const QueryArtistsEndpointHandler: EndpointHandler = async (req: any, res: any) => {
- if (!api.checkQueryArtistsRequest(req)) {
+ if (!api.checkQueryArtistsRequest(req.body)) {
const e: EndpointError = {
internalMessage: 'Invalid QueryArtists request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
- await models.Artist.findAll()
+ const reqObject: api.QueryArtistsRequest = req.body;
+
+ await models.Artist.findAll({
+ offset: reqObject.offset,
+ limit: reqObject.limit,
+ })
.then((artists: any[]) => {
const response: api.QueryArtistsResponse = {
ids: artists.map((artist: any) => {
diff --git a/server/endpoints/QuerySongsEndpointHandler.ts b/server/endpoints/QuerySongsEndpointHandler.ts
index 03e978f..7cf7e09 100644
--- a/server/endpoints/QuerySongsEndpointHandler.ts
+++ b/server/endpoints/QuerySongsEndpointHandler.ts
@@ -8,11 +8,13 @@ const sequelizeOps: any = {
[api.SongQueryFilterOp.Ne]: Op.ne,
[api.SongQueryFilterOp.In]: Op.in,
[api.SongQueryFilterOp.NotIn]: Op.notIn,
+ [api.SongQueryFilterOp.Like]: Op.like,
[api.SongQueryElemOp.And]: Op.and,
[api.SongQueryElemOp.Or]: Op.or,
};
const sequelizeProps: any = {
+ [api.SongQueryElemProperty.title]: "title",
[api.SongQueryElemProperty.id]: "id",
[api.SongQueryElemProperty.artistIds]: "$Artists.id$",
[api.SongQueryElemProperty.albumIds]: "$Albums.id$",
@@ -45,7 +47,7 @@ const getSequelizeWhere = (queryElem: api.SongQueryElem) => {
}
export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res: any) => {
- if (!api.checkQuerySongsRequest(req)) {
+ if (!api.checkQuerySongsRequest(req.body)) {
const e: EndpointError = {
internalMessage: 'Invalid QuerySongs request: ' + JSON.stringify(req.body),
httpStatus: 400
@@ -54,17 +56,39 @@ export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res:
}
const reqObject: api.QuerySongsRequest = req.body;
- await models.Song.findAll({
- where: getSequelizeWhere(reqObject.query),
- include: [models.Artist, models.Album]
- })
- .then((songs: any[]) => {
- const response: api.QuerySongsResponse = {
- ids: songs.map((song: any) => {
- return song.id;
- })
- };
- res.send(response);
+ try {
+ const songs = await models.Song.findAll({
+ where: getSequelizeWhere(reqObject.query),
+ include: [models.Artist, models.Album, models.Tag],
+ limit: reqObject.limit,
+ offset: reqObject.offset,
})
- .catch(catchUnhandledErrors);
+
+ const response: api.QuerySongsResponse = {
+ songs: await Promise.all(songs.map(async (song: any) => {
+ console.log("Song:", song, "artists:", song.getArtists());
+ const artists = await song.getArtists();
+ const tags = await song.getTags();
+ return {
+ id: song.id,
+ title: song.title,
+ artists: artists.map((artist: any) => {
+ return {
+ id: artist.id,
+ name: artist.name,
+ }
+ }),
+ tags: tags.map((tag: any) => {
+ return {
+ id: tag.id,
+ name: tag.name,
+ }
+ })
+ };
+ }))
+ };
+ res.send(response);
+ } catch (e) {
+ catchUnhandledErrors(e);
+ }
}
\ No newline at end of file