diff --git a/client/package.json b/client/package.json
index 5c46bb5..b430876 100644
--- a/client/package.json
+++ b/client/package.json
@@ -15,6 +15,7 @@
"@types/react-dom": "^16.9.0",
"@types/react-router": "^5.1.8",
"@types/react-router-dom": "^5.1.5",
+ "jsurl": "^0.1.5",
"lodash": "^4.17.19",
"material-table": "^1.64.0",
"react": "^16.13.1",
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 0a9bf6e..bb39ca6 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -8,7 +8,7 @@ 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 { SongQuery, toApiQuery, isSongQuery } from './types/Query';
import { SongDisplayItem, ArtistDisplayItem } from './types/DisplayItem';
import { ReactComponent as GooglePlayIcon } from './assets/googleplaymusic_icon.svg';
@@ -17,9 +17,11 @@ import {
Switch,
Route,
useHistory,
+ useLocation,
Redirect
} from "react-router-dom";
-import { timeLog } from 'console';
+
+const JSURL = require('jsurl');
interface SongItemProps {
song: serverApi.SongDetails,
@@ -46,13 +48,12 @@ function SongItem(props: SongItemProps) {
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
- // }
- // })
+ storeLinks: props.song.storeLinks && props.song.storeLinks.map((url: String) => {
+ return {
+ icon: getStoreIcon(url),
+ url: url
+ }
+ }) || [],
}
return ;
@@ -137,18 +138,57 @@ function ArtistList() {
function AppBody() {
const history = useHistory();
- const [songQuery, setSongQuery] = useState({
- 'titleLike': ''
- });
+ const location = useLocation();
+ const queryParams = new URLSearchParams(location.search);
+
+ // If we have an invalid query, change to the default one.
+ const songQuery: SongQuery | undefined = JSURL.tryParse(queryParams.get('query'), undefined);
+
const [songs, setSongs] = useState([]);
+ const offset: number | undefined = queryParams.get('offset') ? parseInt(queryParams.get('offset') || '0') : undefined;
+ const limit: number | undefined = queryParams.get('limit') ? parseInt(queryParams.get('limit') || '0') : undefined;
- React.useEffect(() => {
- const query = songQuery;
+ const fixQueryParams = () => {
+ var fixed = false;
+ if (!isSongQuery(songQuery)) {
+ console.log("query");
+ queryParams.set('query', JSURL.stringify({
+ 'titleLike': ''
+ }));
+ fixed = true;
+ }
+ if (offset == undefined) {
+ console.log("offset", offset);
+ queryParams.set('offset', '0');
+ fixed = true;
+ }
+ if (limit == undefined) {
+ console.log("limit");
+ queryParams.set('limit', '20');
+ fixed = true;
+ }
+ console.log("fixed", fixed);
+ return fixed;
+ }
+
+ const pushQueryParams = () => {
+ history.push({
+ search: "?" + queryParams.toString()
+ })
+ }
+
+ useEffect(() => {
+ if (fixQueryParams()) {
+ pushQueryParams();
+ return;
+ }
+
+ const query: SongQuery = songQuery || { 'titleLike': '' };
setSongs([]);
const request: serverApi.QuerySongsRequest = {
query: toApiQuery(query),
- offset: 0,
- limit: 20,
+ offset: offset || 0,
+ limit: limit || 0,
}
const requestOpts = {
method: 'POST',
@@ -160,7 +200,7 @@ function AppBody() {
.then((json: any) => {
'songs' in json && query === songQuery && setSongs(json.songs);
});
- }, [songQuery]);
+ }, [location]);
const onAppBarTabChange = (value: AppBarActiveTab) => {
switch (value) {
@@ -178,12 +218,17 @@ function AppBody() {
return (
-
+
{ setSongQuery(query); }}
+ onChangeQuery={(squery: SongQuery) => {
+ if (squery != songQuery) {
+ queryParams.set('query', JSURL.stringify(squery));
+ pushQueryParams();
+ }
+ }}
/>
diff --git a/client/src/api.ts b/client/src/api.ts
index f0d31c4..6ff2174 100644
--- a/client/src/api.ts
+++ b/client/src/api.ts
@@ -10,17 +10,20 @@
export interface ArtistDetails {
id: Number,
name: String,
+ storeLinks?: String[],
}
export interface TagDetails {
id: Number,
name: String,
parent?: TagDetails,
+ storeLinks?: String[],
}
export interface SongDetails {
id: Number,
title: String,
artists?: ArtistDetails[],
tags?: TagDetails[],
+ storeLinks?: String[],
}
// Query for songs (POST).
@@ -39,8 +42,8 @@ export enum SongQueryFilterOp {
export enum SongQueryElemProperty {
title = "title",
id = "id",
- artistIds = "artistIds",
- albumIds = "albumIds",
+ artistNames = "artistNames",
+ albumNames = "albumNames",
}
export interface SongQueryElem {
prop?: SongQueryElemProperty,
@@ -52,8 +55,8 @@ export interface SongQueryElem {
export interface SongQuery extends SongQueryElem { }
export interface QuerySongsRequest {
query: SongQuery,
- offset: Number,
- limit: Number,
+ offset: number,
+ limit: number,
}
export interface QuerySongsResponse {
songs: SongDetails[]
diff --git a/client/src/components/FilterControl.tsx b/client/src/components/FilterControl.tsx
index 92e8e7c..7265873 100644
--- a/client/src/components/FilterControl.tsx
+++ b/client/src/components/FilterControl.tsx
@@ -46,14 +46,14 @@ function ArtistFilterControl(props: ArtistFilterControlProps) {
}
export interface IProps {
- query: SongQuery,
+ query: SongQuery | undefined,
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') ||
+ const selectOption: string = (props.query && isTitleQuery(props.query) && 'Title') ||
+ (props.query && isArtistQuery(props.query) && 'Artist') ||
"Unknown";
const handleQueryOnChange = (event: any) => {
@@ -82,7 +82,7 @@ export default function FilterControl(props: IProps) {
return
})}
- {isTitleQuery(props.query) && }
- {isArtistQuery(props.query) && }
+ {props.query && isTitleQuery(props.query) && }
+ {props.query && isArtistQuery(props.query) && }
;
}
\ No newline at end of file
diff --git a/client/src/types/Query.tsx b/client/src/types/Query.tsx
index 4396088..9b2836c 100644
--- a/client/src/types/Query.tsx
+++ b/client/src/types/Query.tsx
@@ -22,10 +22,18 @@ export function isArtistQuery(q: SongQuery): q is ArtistQuery {
}
export function ArtistToApiQuery(q: ArtistQuery) {
return {
+ 'prop': SongQueryElemProperty.artistNames,
+ 'propOperand': '%' + q.artistLike + '%',
+ 'propOperator': SongQueryFilterOp.Like,
}
}
export type SongQuery = TitleQuery | ArtistQuery;
+export function isSongQuery(q: any): q is SongQuery {
+ return q != null &&
+ (isTitleQuery(q) || isArtistQuery(q));
+}
+
export function toApiQuery(q: SongQuery) {
return (isTitleQuery(q) && TitleToApiQuery(q)) ||
(isArtistQuery(q) && ArtistToApiQuery(q)) || {};
diff --git a/client/yarn.lock b/client/yarn.lock
index df9b111..2f8175c 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -6736,6 +6736,11 @@ jss@^10.0.3, jss@^10.3.0:
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
+jsurl@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/jsurl/-/jsurl-0.1.5.tgz#2a5c8741de39cacafc12f448908bf34e960dcee8"
+ integrity sha1-KlyHQd45ysr8EvRIkIvzTpYNzug=
+
jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f"
diff --git a/server/endpoints/QuerySongsEndpointHandler.ts b/server/endpoints/QuerySongsEndpointHandler.ts
index 7cf7e09..f9e2b6b 100644
--- a/server/endpoints/QuerySongsEndpointHandler.ts
+++ b/server/endpoints/QuerySongsEndpointHandler.ts
@@ -16,8 +16,8 @@ const sequelizeOps: any = {
const sequelizeProps: any = {
[api.SongQueryElemProperty.title]: "title",
[api.SongQueryElemProperty.id]: "id",
- [api.SongQueryElemProperty.artistIds]: "$Artists.id$",
- [api.SongQueryElemProperty.albumIds]: "$Albums.id$",
+ [api.SongQueryElemProperty.artistNames]: "$Artists.name$",
+ [api.SongQueryElemProperty.albumNames]: "$Albums.name$",
};
// Returns the "where" clauses for Sequelize, per object type.
@@ -57,21 +57,24 @@ export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res:
const reqObject: api.QuerySongsRequest = req.body;
try {
+ console.log('Song query:', reqObject.query, "where: ", getSequelizeWhere(reqObject.query))
const songs = await models.Song.findAll({
+ // NOTE: have to disable limit and offset because of bug: https://github.com/sequelize/sequelize/issues/11938.
+ // Custom pagination is implemented before responding.
where: getSequelizeWhere(reqObject.query),
include: [models.Artist, models.Album, models.Tag],
- limit: reqObject.limit,
- offset: reqObject.offset,
+ //limit: reqObject.limit,
+ //offset: reqObject.offset,
})
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,
+ storeLinks: song.storeLinks,
artists: artists.map((artist: any) => {
return {
id: artist.id,
@@ -83,9 +86,10 @@ export const QuerySongsEndpointHandler: EndpointHandler = async (req: any, res:
id: tag.id,
name: tag.name,
}
- })
+ }),
};
- }))
+ }).slice(reqObject.offset, reqObject.offset + reqObject.limit))
+ // TODO: custom pagination due to bug mentioned above
};
res.send(response);
} catch (e) {