diff --git a/client/src/api.ts b/client/src/api.ts
index 01ce335..fca7f2d 100644
--- a/client/src/api.ts
+++ b/client/src/api.ts
@@ -121,6 +121,7 @@ export interface QueryResponse {
tags: TagDetails[] | number[] | number,
albums: AlbumDetails[] | number[] | number,
}
+// Note: use -1 as an infinity limit.
export interface OffsetsLimits {
songOffset?: number,
songLimit?: number,
diff --git a/client/src/components/MainWindow.tsx b/client/src/components/MainWindow.tsx
index d3737ae..55b1a6f 100644
--- a/client/src/components/MainWindow.tsx
+++ b/client/src/components/MainWindow.tsx
@@ -73,28 +73,28 @@ export default function MainWindow(props: any) {
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/client/src/components/appbar/AppBar.tsx b/client/src/components/appbar/AppBar.tsx
index fc967ad..ae6062d 100644
--- a/client/src/components/appbar/AppBar.tsx
+++ b/client/src/components/appbar/AppBar.tsx
@@ -3,24 +3,30 @@ import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography,
import SearchIcon from '@material-ui/icons/Search';
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
+import InfoIcon from '@material-ui/icons/Info';
import BuildIcon from '@material-ui/icons/Build';
import { Link, useHistory } from 'react-router-dom';
import { useAuth } from '../../lib/useAuth';
export enum AppBarTab {
- Query = 0,
+ Browse = 0,
+ Query,
Manage,
}
export const appBarTabProps: Record = {
[AppBarTab.Query]: {
- label: Query,
+ label: Query,
path: "/query",
},
[AppBarTab.Manage]: {
- label: Manage,
+ label: Manage,
path: "/manage",
},
+ [AppBarTab.Browse]: {
+ label: Browse,
+ path: undefined,
+ },
}
export function UserMenu(props: {
@@ -86,13 +92,17 @@ export default function AppBar(props: {
{auth.user && history.push(appBarTabProps[val].path)}
+ onChange={(e: any, val: AppBarTab) => {
+ let path = appBarTabProps[val].path
+ path && history.push(appBarTabProps[val].path)
+ }}
variant="scrollable"
scrollButtons="auto"
>
{Object.keys(appBarTabProps).map((tab: any, idx: number) => )}
}
diff --git a/client/src/components/common/StoreLinkIcon.tsx b/client/src/components/common/StoreLinkIcon.tsx
index 72f600f..1a8868d 100644
--- a/client/src/components/common/StoreLinkIcon.tsx
+++ b/client/src/components/common/StoreLinkIcon.tsx
@@ -13,15 +13,21 @@ export interface IProps {
whichStore: ExternalStore,
}
+// Links to external stores are identified by their domain or some
+// other unique substring. These unique substrings are stored here.
+export const StoreURLIdentifiers: Record = {
+ [ExternalStore.GooglePlayMusic]: 'play.google.com',
+ [ExternalStore.Spotify]: 'spotify.com',
+ [ExternalStore.YoutubeMusic]: 'music.youtube.com',
+}
+
export function whichStore(url: string) {
- if (url.includes('play.google.com')) {
- return ExternalStore.GooglePlayMusic;
- } else if (url.includes('spotify.com')) {
- return ExternalStore.Spotify;
- } else if (url.includes('music.youtube.com')) {
- return ExternalStore.YoutubeMusic;
- }
- return undefined;
+ return Object.keys(StoreURLIdentifiers).reduce((prev: string | undefined, cur: string) => {
+ if(url.includes(StoreURLIdentifiers[cur as ExternalStore])) {
+ return cur;
+ }
+ return prev;
+ }, undefined);
}
export default function StoreLinkIcon(props: any) {
diff --git a/client/src/components/windows/album/AlbumWindow.tsx b/client/src/components/windows/album/AlbumWindow.tsx
index 85b40e0..64bfd04 100644
--- a/client/src/components/windows/album/AlbumWindow.tsx
+++ b/client/src/components/windows/album/AlbumWindow.tsx
@@ -49,22 +49,20 @@ export function AlbumWindowReducer(state: AlbumWindowState, action: any) {
}
export async function getAlbumMetadata(id: number) {
- return (await queryAlbums({
- query: {
+ let result: any = await queryAlbums(
+ {
a: QueryLeafBy.AlbumId,
b: id,
leafOp: QueryLeafOp.Equals,
- },
- offset: 0,
- limit: 1,
- })
- )[0];
+ }, 0, 1, serverApi.QueryResponseType.Details
+ );
+ return result[0];
}
export default function AlbumWindow(props: {}) {
- const { id } = useParams();
+ const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(AlbumWindowReducer, {
- id: id,
+ id: parseInt(id),
metadata: null,
pendingChanges: null,
songGetters: songGetters,
@@ -99,16 +97,14 @@ export function AlbumWindowControlled(props: {
if (songsOnAlbum) { return; }
(async () => {
- const songs = await querySongs({
- query: {
+ const songs = await querySongs(
+ {
a: QueryLeafBy.AlbumId,
b: albumId,
leafOp: QueryLeafOp.Equals,
- },
- offset: 0,
- limit: -1,
- })
- .catch((e: any) => { handleNotLoggedIn(auth, e) });
+ }, 0, -1, serverApi.QueryResponseType.Details
+ )
+ .catch((e: any) => { handleNotLoggedIn(auth, e) });
dispatch({
type: AlbumWindowStateActions.SetSongs,
value: songs,
diff --git a/client/src/components/windows/artist/ArtistWindow.tsx b/client/src/components/windows/artist/ArtistWindow.tsx
index 07aaef5..582ead2 100644
--- a/client/src/components/windows/artist/ArtistWindow.tsx
+++ b/client/src/components/windows/artist/ArtistWindow.tsx
@@ -54,21 +54,20 @@ export interface IProps {
}
export async function getArtistMetadata(id: number) {
- return (await queryArtists({
- query: {
+ let response: any = await queryArtists(
+ {
a: QueryLeafBy.ArtistId,
b: id,
leafOp: QueryLeafOp.Equals,
- },
- offset: 0,
- limit: 1,
- }))[0];
+ }, 0, 1, serverApi.QueryResponseType.Details
+ );
+ return response[0];
}
export default function ArtistWindow(props: {}) {
- const { id } = useParams();
+ const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(ArtistWindowReducer, {
- id: id,
+ id: parseInt(id),
metadata: null,
pendingChanges: null,
songGetters: songGetters,
@@ -103,16 +102,14 @@ export function ArtistWindowControlled(props: {
if (songsByArtist) { return; }
(async () => {
- const songs = await querySongs({
- query: {
+ const songs = await querySongs(
+ {
a: QueryLeafBy.ArtistId,
b: artistId,
leafOp: QueryLeafOp.Equals,
- },
- offset: 0,
- limit: -1,
- })
- .catch((e: any) => { handleNotLoggedIn(auth, e) });
+ }, 0, -1, serverApi.QueryResponseType.Details,
+ )
+ .catch((e: any) => { handleNotLoggedIn(auth, e) });
dispatch({
type: ArtistWindowStateActions.SetSongs,
value: songs,
diff --git a/client/src/components/windows/manage_links/LinksStatusWidget.tsx b/client/src/components/windows/manage_links/LinksStatusWidget.tsx
new file mode 100644
index 0000000..20b3fe9
--- /dev/null
+++ b/client/src/components/windows/manage_links/LinksStatusWidget.tsx
@@ -0,0 +1,111 @@
+import { Box, Typography } from '@material-ui/core';
+import React, { useCallback, useEffect, useReducer, useState } from 'react';
+import { $enum } from 'ts-enum-util';
+import { ItemType, QueryElemProperty, QueryResponseType } from '../../../api';
+import { queryItems } from '../../../lib/backend/queries';
+import { QueryElem, QueryLeafBy, QueryLeafOp } from '../../../lib/query/Query';
+import { ExternalStore, StoreURLIdentifiers } from '../../common/StoreLinkIcon';
+
+var _ = require('lodash');
+
+export default function LinksStatusWidget(props: {
+
+}) {
+ type Counts = {
+ songs: number | undefined,
+ albums: number | undefined,
+ artists: number | undefined,
+ };
+
+ let [totalCounts, setTotalCounts] = useState(undefined);
+ let [linkedCounts, setLinkedCounts] = useState>({});
+
+ let queryStoreCount = async (store: ExternalStore, type: ItemType) => {
+ let whichProp: any = {
+ [ItemType.Song]: QueryLeafBy.SongStoreLinks,
+ [ItemType.Artist]: QueryLeafBy.ArtistStoreLinks,
+ [ItemType.Album]: QueryLeafBy.AlbumStoreLinks,
+ }
+ let whichElem: any = {
+ [ItemType.Song]: 'songs',
+ [ItemType.Artist]: 'artists',
+ [ItemType.Album]: 'albums',
+ }
+ let r: any = await queryItems(
+ [type],
+ {
+ a: whichProp[type],
+ leafOp: QueryLeafOp.Like,
+ b: `%${StoreURLIdentifiers[store]}%`,
+ },
+ undefined,
+ undefined,
+ QueryResponseType.Count
+ );
+ return r[whichElem[type]];
+ }
+
+ // Start retrieving total counts
+ useEffect(() => {
+ (async () => {
+ let counts: any = await queryItems(
+ [ItemType.Song, ItemType.Artist, ItemType.Album],
+ undefined,
+ undefined,
+ undefined,
+ QueryResponseType.Count
+ );
+ setTotalCounts(counts);
+ }
+ )();
+ }, []);
+
+ // Start retrieving counts per store
+ useEffect(() => {
+ (async () => {
+ let promises = $enum(ExternalStore).getValues().map((s: ExternalStore) => {
+ let songsPromise: Promise = queryStoreCount(s, ItemType.Song);
+ let albumsPromise: Promise = queryStoreCount(s, ItemType.Album);
+ let artistsPromise: Promise = queryStoreCount(s, ItemType.Artist);
+ let updatePromise = Promise.all([songsPromise, albumsPromise, artistsPromise]).then(
+ (r: any[]) => {
+ setLinkedCounts((prev: Record) => {
+ return {
+ ...prev,
+ [s]: {
+ songs: r[0],
+ artists: r[2],
+ albums: r[1],
+ }
+ }
+ });
+ }
+ )
+ console.log(s);
+ return updatePromise;
+ })
+ return Promise.all(promises);
+ }
+ )();
+ }, [setLinkedCounts]);
+
+ let storeReady = (s: ExternalStore) => {
+ return s in linkedCounts;
+ }
+
+ return <>
+ {$enum(ExternalStore).getValues().map((s: ExternalStore) => {
+ return
+ {totalCounts && storeReady(s) &&
+
+ {s}:
+ {linkedCounts[s].songs} / {totalCounts.songs} songs linked
+ {linkedCounts[s].artists} / {totalCounts.artists} artists linked
+ {linkedCounts[s].albums} / {totalCounts.albums} albums linked
+
+
+ }
+
+ })}
+ >
+}
\ No newline at end of file
diff --git a/client/src/components/windows/manage_links/ManageLinksWindow.tsx b/client/src/components/windows/manage_links/ManageLinksWindow.tsx
index 3960308..77b1b29 100644
--- a/client/src/components/windows/manage_links/ManageLinksWindow.tsx
+++ b/client/src/components/windows/manage_links/ManageLinksWindow.tsx
@@ -5,6 +5,8 @@ import { useHistory } from 'react-router';
import { useAuth, Auth } from '../../../lib/useAuth';
import Alert from '@material-ui/lab/Alert';
import { Link } from 'react-router-dom';
+import OpenInNewIcon from '@material-ui/icons/OpenInNew';
+import LinksStatusWidget from './LinksStatusWidget';
export interface ManageLinksWindowState extends WindowState {
dummy: boolean
@@ -34,5 +36,27 @@ export function ManageLinksWindowControlled(props: {
state: ManageLinksWindowState,
dispatch: (action: any) => void,
}) {
- return <>Hi!>;
+ return
+
+
+
+
+ Manage Links
+
+
+
+
+ ;
}
\ No newline at end of file
diff --git a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx
index 14e47e4..5ce2e07 100644
--- a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx
+++ b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx
@@ -13,6 +13,7 @@ import Alert from '@material-ui/lab/Alert';
import { useHistory } from 'react-router';
import { NotLoggedInError, handleNotLoggedIn } from '../../../lib/backend/request';
import { useAuth } from '../../../lib/useAuth';
+import * as serverApi from '../../../api';
var _ = require('lodash');
export interface ManageTagsWindowState extends WindowState {
@@ -79,11 +80,9 @@ export function organiseTags(allTags: Record, fromId: string | null
export async function getAllTags() {
return (async () => {
var retval: Record = {};
- const tags = await queryTags({
- query: undefined,
- offset: 0,
- limit: -1,
- });
+ const tags: any = await queryTags(
+ undefined, 0, -1, serverApi.QueryResponseType.Details,
+ );
// Convert numeric IDs to string IDs because that is
// what we work with within this component.
tags.forEach((tag: any) => {
@@ -426,13 +425,13 @@ export function ManageTagsWindowControlled(props: {
type: ManageTagsWindowActions.Reset
});
})
- .catch((e: any) => { handleNotLoggedIn(auth, e) })
- .catch((e: Error) => {
- props.dispatch({
- type: ManageTagsWindowActions.SetAlert,
- value: Failed to save changes: {e.message},
+ .catch((e: any) => { handleNotLoggedIn(auth, e) })
+ .catch((e: Error) => {
+ props.dispatch({
+ type: ManageTagsWindowActions.SetAlert,
+ value: Failed to save changes: {e.message},
+ })
})
- })
}}
getTagDetails={(id: string) => tagsWithChanges[id]}
/>
diff --git a/client/src/components/windows/query/QueryWindow.tsx b/client/src/components/windows/query/QueryWindow.tsx
index 85afec5..9a428cc 100644
--- a/client/src/components/windows/query/QueryWindow.tsx
+++ b/client/src/components/windows/query/QueryWindow.tsx
@@ -6,6 +6,7 @@ import SongTable from '../../tables/ResultsTable';
import { songGetters } from '../../../lib/songGetters';
import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows';
+import { QueryResponseType } from '../../../api';
var _ = require('lodash');
export interface ResultsForQuery {
@@ -26,53 +27,51 @@ export enum QueryWindowStateActions {
}
async function getArtistNames(filter: string) {
- const artists = await queryArtists({
- query: filter.length > 0 ? {
+ const artists: any = await queryArtists(
+ filter.length > 0 ? {
a: QueryLeafBy.ArtistName,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
- offset: 0,
- limit: -1,
- });
+ 0, -1, QueryResponseType.Details
+ );
return [...(new Set([...(artists.map((a: any) => a.name))]))];
}
async function getAlbumNames(filter: string) {
- const albums = await queryAlbums({
- query: filter.length > 0 ? {
+ const albums: any = await queryAlbums(
+ filter.length > 0 ? {
a: QueryLeafBy.AlbumName,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
- offset: 0,
- limit: -1,
- });
+ 0, -1, QueryResponseType.Details
+ );
return [...(new Set([...(albums.map((a: any) => a.name))]))];
}
async function getSongTitles(filter: string) {
- const songs = await querySongs({
- query: filter.length > 0 ? {
+ const songs: any = await querySongs(
+ filter.length > 0 ? {
a: QueryLeafBy.SongTitle,
b: '%' + filter + '%',
leafOp: QueryLeafOp.Like
} : undefined,
- offset: 0,
- limit: -1,
- });
+ 0, -1, QueryResponseType.Details
+ );
return [...(new Set([...(songs.map((s: any) => s.title))]))];
}
-async function getTagItems() {
- return await queryTags({
- query: undefined,
- offset: 0,
- limit: -1,
- });
+async function getTagItems(): Promise {
+ let tags: any = await queryTags(
+ undefined,
+ 0, -1, QueryResponseType.Details
+ );
+
+ return tags;
}
export function QueryWindowReducer(state: QueryWindowState, action: any) {
@@ -112,17 +111,18 @@ export function QueryWindowControlled(props: {
}
let setResultsForQuery = useCallback((r: ResultsForQuery | null) => {
dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r });
- }, [ dispatch ]);
+ }, [dispatch]);
const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query));
const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : [];
const doQuery = useCallback(async (_query: QueryElem) => {
- const songs = await querySongs({
- query: _query,
- offset: 0,
- limit: 100, //TODO: pagination
- });
+ const songs: any = await querySongs(
+ _query,
+ 0,
+ 100, //TODO: pagination
+ QueryResponseType.Details
+ );
if (_.isEqual(query, _query)) {
setResultsForQuery({
diff --git a/client/src/components/windows/song/SongWindow.tsx b/client/src/components/windows/song/SongWindow.tsx
index 288f307..832aca0 100644
--- a/client/src/components/windows/song/SongWindow.tsx
+++ b/client/src/components/windows/song/SongWindow.tsx
@@ -39,21 +39,20 @@ export function SongWindowReducer(state: SongWindowState, action: any) {
}
export async function getSongMetadata(id: number) {
- return (await querySongs({
- query: {
+ let response: any = await querySongs(
+ {
a: QueryLeafBy.SongId,
b: id,
leafOp: QueryLeafOp.Equals,
- },
- offset: 0,
- limit: 1,
- }))[0];
+ }, 0, 1, serverApi.QueryResponseType.Details
+ );
+ return response[0];
}
export default function SongWindow(props: {}) {
- const { id } = useParams();
+ const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(SongWindowReducer, {
- id: id,
+ id: parseInt(id),
metadata: null,
});
diff --git a/client/src/components/windows/tag/TagWindow.tsx b/client/src/components/windows/tag/TagWindow.tsx
index 49f283f..5327eef 100644
--- a/client/src/components/windows/tag/TagWindow.tsx
+++ b/client/src/components/windows/tag/TagWindow.tsx
@@ -52,15 +52,15 @@ export function TagWindowReducer(state: TagWindowState, action: any) {
}
export async function getTagMetadata(id: number) {
- var tag = (await queryTags({
- query: {
+ let tags: any = await queryTags(
+ {
a: QueryLeafBy.TagId,
b: id,
leafOp: QueryLeafOp.Equals,
- },
- offset: 0,
- limit: 1,
- }))[0];
+ }, 0, 1, serverApi.QueryResponseType.Details
+ );
+
+ var tag = tags[0];
// Recursively fetch parent tags to build the full metadata.
if (tag.parentId) {
@@ -76,9 +76,9 @@ export async function getTagMetadata(id: number) {
}
export default function TagWindow(props: {}) {
- const { id } = useParams();
+ const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(TagWindowReducer, {
- id: id,
+ id: parseInt(id),
metadata: null,
pendingChanges: null,
songGetters: songGetters,
@@ -113,15 +113,13 @@ export function TagWindowControlled(props: {
if (songsWithTag) { return; }
(async () => {
- const songs = await querySongs({
- query: {
+ const songs: any = await querySongs(
+ {
a: QueryLeafBy.TagId,
b: tagId,
leafOp: QueryLeafOp.Equals,
- },
- offset: 0,
- limit: -1,
- });
+ }, 0, -1, serverApi.QueryResponseType.Details,
+ );
dispatch({
type: TagWindowStateActions.SetSongs,
value: songs,
diff --git a/client/src/lib/backend/queries.tsx b/client/src/lib/backend/queries.tsx
index d6c1aa9..593e5e0 100644
--- a/client/src/lib/backend/queries.tsx
+++ b/client/src/lib/backend/queries.tsx
@@ -2,18 +2,39 @@ import * as serverApi from '../../api';
import { QueryElem, toApiQuery } from '../query/Query';
import backendRequest from './request';
-export interface QueryArgs {
- query?: QueryElem,
- offset: number,
- limit: number,
-}
-
-export async function queryArtists(args: QueryArgs) {
+export async function queryItems(
+ types: serverApi.ItemType[],
+ query: QueryElem | undefined,
+ offset: number | undefined,
+ limit: number | undefined,
+ responseType: serverApi.QueryResponseType,
+): Promise<{
+ artists: serverApi.ArtistDetails[],
+ albums: serverApi.AlbumDetails[],
+ tags: serverApi.TagDetails[],
+ songs: serverApi.SongDetails[],
+} | {
+ artists: number[],
+ albums: number[],
+ tags: number[],
+ songs: number[],
+} | {
+ artists: number,
+ albums: number,
+ tags: number,
+ songs: number,
+}> {
var q: serverApi.QueryRequest = {
- query: args.query ? toApiQuery(args.query) : {},
+ query: query ? toApiQuery(query) : {},
offsetsLimits: {
- artistOffset: args.offset,
- artistLimit: args.limit,
+ artistOffset: (serverApi.ItemType.Artist in types) ? (offset || 0) : undefined,
+ artistLimit: (serverApi.ItemType.Artist in types) ? (limit || -1) : undefined,
+ albumOffset: (serverApi.ItemType.Album in types) ? (offset || 0) : undefined,
+ albumLimit: (serverApi.ItemType.Album in types) ? (limit || -1) : undefined,
+ songOffset: (serverApi.ItemType.Song in types) ? (offset || 0) : undefined,
+ songLimit: (serverApi.ItemType.Song in types) ? (limit || -1) : undefined,
+ tagOffset: (serverApi.ItemType.Tag in types) ? (offset || 0) : undefined,
+ tagLimit: (serverApi.ItemType.Tag in types) ? (limit || -1) : undefined,
},
ordering: {
orderBy: {
@@ -21,6 +42,7 @@ export async function queryArtists(args: QueryArgs) {
},
ascending: true,
},
+ responseType: responseType,
};
const requestOpts = {
@@ -32,110 +54,174 @@ export async function queryArtists(args: QueryArgs) {
return (async () => {
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json();
- return json.artists;
+ return json;
})();
}
-export async function queryAlbums(args: QueryArgs) {
- var q: serverApi.QueryRequest = {
- query: args.query ? toApiQuery(args.query) : {},
- offsetsLimits: {
- albumOffset: args.offset,
- albumLimit: args.limit,
- },
- ordering: {
- orderBy: {
- type: serverApi.OrderByType.Name,
- },
- ascending: true,
- },
- };
-
- const requestOpts = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(q),
- };
-
- return (async () => {
- const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
- let json: any = await response.json();
- return json.albums;
- })();
+export async function queryArtists(
+ query: QueryElem | undefined,
+ offset: number | undefined,
+ limit: number | undefined,
+ responseType: serverApi.QueryResponseType,
+): Promise {
+ let r = await queryItems([serverApi.ItemType.Artist], query, offset, limit, responseType);
+ return r.artists;
+
+ // var q: serverApi.QueryRequest = {
+ // query: query ? toApiQuery(query) : {},
+ // offsetsLimits: {
+ // artistOffset: offset,
+ // artistLimit: limit,
+ // },
+ // ordering: {
+ // orderBy: {
+ // type: serverApi.OrderByType.Name,
+ // },
+ // ascending: true,
+ // },
+ // responseType: responseType,
+ // };
+
+ // const requestOpts = {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify(q),
+ // };
+
+ // return (async () => {
+ // const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ // let json: any = await response.json();
+ // return json.artists;
+ // })();
}
-export async function querySongs(args: QueryArgs) {
- var q: serverApi.QueryRequest = {
- query: args.query ? toApiQuery(args.query) : {},
- offsetsLimits: {
- songOffset: args.offset,
- songLimit: args.limit,
- },
- ordering: {
- orderBy: {
- type: serverApi.OrderByType.Name,
- },
- ascending: true,
- },
- };
-
- const requestOpts = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(q),
- };
-
- return (async () => {
- const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
- let json: any = await response.json();
- return json.songs;
- })();
+export async function queryAlbums(
+ query: QueryElem | undefined,
+ offset: number | undefined,
+ limit: number | undefined,
+ responseType: serverApi.QueryResponseType,
+): Promise {
+ let r = await queryItems([serverApi.ItemType.Album], query, offset, limit, responseType);
+ return r.albums;
+
+ // var q: serverApi.QueryRequest = {
+ // query: query ? toApiQuery(query) : {},
+ // offsetsLimits: {
+ // albumOffset: offset,
+ // albumLimit: limit,
+ // },
+ // ordering: {
+ // orderBy: {
+ // type: serverApi.OrderByType.Name,
+ // },
+ // ascending: true,
+ // },
+ // responseType: responseType,
+ // };
+
+ // const requestOpts = {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify(q),
+ // };
+
+ // return (async () => {
+ // const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ // let json: any = await response.json();
+ // return json.albums;
+ // })();
}
-export async function queryTags(args: QueryArgs) {
- var q: serverApi.QueryRequest = {
- query: args.query ? toApiQuery(args.query) : {},
- offsetsLimits: {
- tagOffset: args.offset,
- tagLimit: args.limit,
- },
- ordering: {
- orderBy: {
- type: serverApi.OrderByType.Name,
- },
- ascending: true,
- },
- };
-
- const requestOpts = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(q),
- };
+export async function querySongs(
+ query: QueryElem | undefined,
+ offset: number | undefined,
+ limit: number | undefined,
+ responseType: serverApi.QueryResponseType,
+): Promise {
+ let r = await queryItems([serverApi.ItemType.Song], query, offset, limit, responseType);
+ return r.songs;
+
+ // var q: serverApi.QueryRequest = {
+ // query: query ? toApiQuery(query) : {},
+ // offsetsLimits: {
+ // songOffset: offset,
+ // songLimit: limit,
+ // },
+ // ordering: {
+ // orderBy: {
+ // type: serverApi.OrderByType.Name,
+ // },
+ // ascending: true,
+ // },
+ // responseType: responseType,
+ // };
+
+ // const requestOpts = {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify(q),
+ // };
+
+ // return (async () => {
+ // const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
+ // let json: any = await response.json();
+ // return json.songs;
+ // })();
+}
- return (async () => {
- const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts);
- let json: any = await response.json();
- const tags = json.tags;
-
- // Organise the tags into a tree structure.
- // First, we put them in an indexed dict.
- const idxTags: Record = {};
- tags.forEach((tag: any) => {
- idxTags[tag.tagId] = {
- ...tag,
- childIds: [],
- }
- })
-
- // Resolve children.
- tags.forEach((tag: any) => {
- if(tag.parentId && tag.parentId in idxTags) {
- idxTags[tag.parentId].childIds.push(tag.tagId);
- }
- })
-
- // Return the loose objects again.
- return Object.values(idxTags);
- })();
+export async function queryTags(
+ query: QueryElem | undefined,
+ offset: number | undefined,
+ limit: number | undefined,
+ responseType: serverApi.QueryResponseType,
+): Promise {
+ let r = await queryItems([serverApi.ItemType.Tag], query, offset, limit, responseType);
+ return r.tags;
+
+ // var q: serverApi.QueryRequest = {
+ // query: query ? toApiQuery(query) : {},
+ // offsetsLimits: {
+ // tagOffset: offset,
+ // tagLimit: limit,
+ // },
+ // ordering: {
+ // orderBy: {
+ // type: serverApi.OrderByType.Name,
+ // },
+ // ascending: true,
+ // },
+ // responseType: responseType,
+ // };
+
+ // const requestOpts = {
+ // method: 'POST',
+ // headers: { 'Content-Type': 'application/json' },
+ // body: JSON.stringify(q),
+ // };
+
+ // return (async () => {
+ // const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts);
+ // let json: any = await response.json();
+ // const tags = json.tags;
+
+ // // Organise the tags into a tree structure.
+ // // First, we put them in an indexed dict.
+ // const idxTags: Record = {};
+ // tags.forEach((tag: any) => {
+ // idxTags[tag.tagId] = {
+ // ...tag,
+ // childIds: [],
+ // }
+ // })
+
+ // // Resolve children.
+ // tags.forEach((tag: any) => {
+ // if (tag.parentId && tag.parentId in idxTags) {
+ // idxTags[tag.parentId].childIds.push(tag.tagId);
+ // }
+ // })
+
+ // // Return the loose objects again.
+ // return Object.values(idxTags);
+ // })();
}
\ No newline at end of file
diff --git a/client/src/lib/backend/request.tsx b/client/src/lib/backend/request.tsx
index 0195068..f817a58 100644
--- a/client/src/lib/backend/request.tsx
+++ b/client/src/lib/backend/request.tsx
@@ -10,7 +10,7 @@ export class NotLoggedInError extends Error {
}
export function isNotLoggedInError(e: any): e is NotLoggedInError {
- return e.name === NotLoggedInError;
+ return e.name === "NotLoggedInError";
}
export default async function backendRequest(url: any, ...restArgs: any[]): Promise {
@@ -23,6 +23,7 @@ export default async function backendRequest(url: any, ...restArgs: any[]): Prom
}
export function handleNotLoggedIn(auth: Auth, e: any) {
+ console.log("Error:", e);
if (isNotLoggedInError(e)) {
console.log("Not logged in!")
auth.signout();
diff --git a/client/src/lib/integration/useIntegrations.tsx b/client/src/lib/integration/useIntegrations.tsx
index 7eb9ac1..0960862 100644
--- a/client/src/lib/integration/useIntegrations.tsx
+++ b/client/src/lib/integration/useIntegrations.tsx
@@ -14,7 +14,7 @@ export type IntegrationState = {
};
export type IntegrationsState = IntegrationState[] | "Loading";
-export function isIntegrationState(v: any) : v is IntegrationState {
+export function isIntegrationState(v: any): v is IntegrationState {
return 'id' in v && 'integration' in v && 'properties' in v;
}
@@ -31,9 +31,9 @@ export const IntegrationClasses: Record = {
[serverApi.IntegrationType.YoutubeWebScraper]: YoutubeMusicWebScraper,
}
-export function makeDefaultIntegrationProperties(type: serverApi.IntegrationType):
+export function makeDefaultIntegrationProperties(type: serverApi.IntegrationType):
serverApi.CreateIntegrationRequest {
- switch(type) {
+ switch (type) {
case serverApi.IntegrationType.SpotifyClientCredentials: {
return {
name: "Spotify App",
@@ -57,7 +57,7 @@ export function makeDefaultIntegrationProperties(type: serverApi.IntegrationType
}
export function makeIntegration(p: serverApi.CreateIntegrationRequest, id: number) {
- switch(p.type) {
+ switch (p.type) {
case serverApi.IntegrationType.SpotifyClientCredentials: {
return new SpotifyClientCreds(id);
}
@@ -126,7 +126,7 @@ function useProvideIntegrations(): Integrations {
const [state, dispatch] = useReducer(IntegrationsReducer, [])
let updateFromUpstream = async () => {
- backend.getIntegrations()
+ return await backend.getIntegrations()
.then((integrations: serverApi.ListIntegrationsResponse) => {
dispatch({
type: IntegrationsActions.Set,
@@ -139,22 +139,22 @@ function useProvideIntegrations(): Integrations {
})
});
})
- .catch((e: any) => handleNotLoggedIn(auth, e));
+ .catch((e) => handleNotLoggedIn(auth, e));
}
let addIntegration = async (v: serverApi.CreateIntegrationRequest) => {
- const id = await backend.createIntegration(v);
+ const id = await backend.createIntegration(v).catch((e: any) => { handleNotLoggedIn(auth, e) });
await updateFromUpstream();
return id;
}
let deleteIntegration = async (id: number) => {
- await backend.deleteIntegration(id);
+ await backend.deleteIntegration(id).catch((e: any) => { handleNotLoggedIn(auth, e) });
await updateFromUpstream();
}
let modifyIntegration = async (id: number, v: serverApi.CreateIntegrationRequest) => {
- await backend.modifyIntegration(id, v);
+ await backend.modifyIntegration(id, v).catch((e: any) => { handleNotLoggedIn(auth, e) });
await updateFromUpstream();
}
diff --git a/client/src/lib/query/Query.tsx b/client/src/lib/query/Query.tsx
index e2a51be..dcd96c7 100644
--- a/client/src/lib/query/Query.tsx
+++ b/client/src/lib/query/Query.tsx
@@ -9,6 +9,9 @@ export enum QueryLeafBy {
TagId,
SongTitle,
SongId,
+ SongStoreLinks,
+ ArtistStoreLinks,
+ AlbumStoreLinks,
}
export enum QueryLeafOp {
@@ -174,6 +177,9 @@ export function toApiQuery(q: QueryElem) : serverApi.Query {
[QueryLeafBy.ArtistId]: serverApi.QueryElemProperty.artistId,
[QueryLeafBy.TagId]: serverApi.QueryElemProperty.tagId,
[QueryLeafBy.SongId]: serverApi.QueryElemProperty.songId,
+ [QueryLeafBy.SongStoreLinks]: serverApi.QueryElemProperty.songStoreLinks,
+ [QueryLeafBy.ArtistStoreLinks]: serverApi.QueryElemProperty.artistStoreLinks,
+ [QueryLeafBy.AlbumStoreLinks]: serverApi.QueryElemProperty.albumStoreLinks,
}
const leafOpsMapping: any = {
[QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq,
diff --git a/client/src/lib/useAuth.tsx b/client/src/lib/useAuth.tsx
index f25dff4..51f208f 100644
--- a/client/src/lib/useAuth.tsx
+++ b/client/src/lib/useAuth.tsx
@@ -112,14 +112,15 @@ function useProvideAuth() {
};
const signout = () => {
+ console.log("Signing out.");
+ setUser(null);
+ persistAuth(null);
return (async () => {
const url = (process.env.REACT_APP_BACKEND || "") + serverApi.LogoutEndpoint;
const response = await fetch(url, { method: "POST" });
if (!response.ok) {
throw new Error("Failed to log out.");
}
- setUser(null);
- persistAuth(null);
})();
};