From e257c135becdab9f41b45b54e65bb755797a1cc3 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Thu, 26 Nov 2020 14:17:18 +0100 Subject: [PATCH] Links window shows total linking stats. --- client/src/api.ts | 1 + client/src/components/MainWindow.tsx | 12 +- client/src/components/appbar/AppBar.tsx | 18 +- .../src/components/common/StoreLinkIcon.tsx | 22 +- .../components/windows/album/AlbumWindow.tsx | 28 +- .../windows/artist/ArtistWindow.tsx | 27 +- .../manage_links/LinksStatusWidget.tsx | 111 +++++++ .../manage_links/ManageLinksWindow.tsx | 26 +- .../windows/manage_tags/ManageTagsWindow.tsx | 21 +- .../components/windows/query/QueryWindow.tsx | 54 ++-- .../components/windows/song/SongWindow.tsx | 15 +- .../src/components/windows/tag/TagWindow.tsx | 26 +- client/src/lib/backend/queries.tsx | 302 +++++++++++------- client/src/lib/backend/request.tsx | 3 +- .../src/lib/integration/useIntegrations.tsx | 18 +- client/src/lib/query/Query.tsx | 6 + client/src/lib/useAuth.tsx | 5 +- 17 files changed, 465 insertions(+), 230 deletions(-) create mode 100644 client/src/components/windows/manage_links/LinksStatusWidget.tsx 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); })(); };