Links window shows total linking stats.

editsong
Sander Vocke 5 years ago
parent c28db21b18
commit e257c135be
  1. 1
      client/src/api.ts
  2. 12
      client/src/components/MainWindow.tsx
  3. 18
      client/src/components/appbar/AppBar.tsx
  4. 22
      client/src/components/common/StoreLinkIcon.tsx
  5. 28
      client/src/components/windows/album/AlbumWindow.tsx
  6. 27
      client/src/components/windows/artist/ArtistWindow.tsx
  7. 111
      client/src/components/windows/manage_links/LinksStatusWidget.tsx
  8. 26
      client/src/components/windows/manage_links/ManageLinksWindow.tsx
  9. 21
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  10. 54
      client/src/components/windows/query/QueryWindow.tsx
  11. 15
      client/src/components/windows/song/SongWindow.tsx
  12. 26
      client/src/components/windows/tag/TagWindow.tsx
  13. 302
      client/src/lib/backend/queries.tsx
  14. 3
      client/src/lib/backend/request.tsx
  15. 18
      client/src/lib/integration/useIntegrations.tsx
  16. 6
      client/src/lib/query/Query.tsx
  17. 5
      client/src/lib/useAuth.tsx

@ -121,6 +121,7 @@ export interface QueryResponse {
tags: TagDetails[] | number[] | number, tags: TagDetails[] | number[] | number,
albums: AlbumDetails[] | number[] | number, albums: AlbumDetails[] | number[] | number,
} }
// Note: use -1 as an infinity limit.
export interface OffsetsLimits { export interface OffsetsLimits {
songOffset?: number, songOffset?: number,
songLimit?: number, songLimit?: number,

@ -73,28 +73,28 @@ export default function MainWindow(props: any) {
<QueryWindow /> <QueryWindow />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path="/artist/:id"> <PrivateRoute path="/artist/:id">
<AppBar selectedTab={null} /> <AppBar selectedTab={AppBarTab.Browse} />
<ArtistWindow /> <ArtistWindow />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path="/tag/:id"> <PrivateRoute path="/tag/:id">
<AppBar selectedTab={null} /> <AppBar selectedTab={AppBarTab.Browse} />
<TagWindow /> <TagWindow />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path="/album/:id"> <PrivateRoute path="/album/:id">
<AppBar selectedTab={null} /> <AppBar selectedTab={AppBarTab.Browse} />
<AlbumWindow /> <AlbumWindow />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path="/song/:id"> <PrivateRoute path="/song/:id">
<AppBar selectedTab={null} /> <AppBar selectedTab={AppBarTab.Browse} />
<SongWindow /> <SongWindow />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path="/manage/tags"> <PrivateRoute path="/manage/tags">
<AppBar selectedTab={AppBarTab.Manage} /> <AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.Tags}/> <ManageWindow selectedWindow={ManageWhat.Tags} />
</PrivateRoute> </PrivateRoute>
<PrivateRoute path="/manage/links"> <PrivateRoute path="/manage/links">
<AppBar selectedTab={AppBarTab.Manage} /> <AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.Links}/> <ManageWindow selectedWindow={ManageWhat.Links} />
</PrivateRoute> </PrivateRoute>
<PrivateRoute exact path="/manage"> <PrivateRoute exact path="/manage">
<Redirect to={"/manage/tags"} /> <Redirect to={"/manage/tags"} />

@ -3,24 +3,30 @@ import { AppBar as MuiAppBar, Box, Tab as MuiTab, Tabs, IconButton, Typography,
import SearchIcon from '@material-ui/icons/Search'; import SearchIcon from '@material-ui/icons/Search';
import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import LocalOfferIcon from '@material-ui/icons/LocalOffer';
import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import InfoIcon from '@material-ui/icons/Info';
import BuildIcon from '@material-ui/icons/Build'; import BuildIcon from '@material-ui/icons/Build';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import { useAuth } from '../../lib/useAuth'; import { useAuth } from '../../lib/useAuth';
export enum AppBarTab { export enum AppBarTab {
Query = 0, Browse = 0,
Query,
Manage, Manage,
} }
export const appBarTabProps: Record<any, any> = { export const appBarTabProps: Record<any, any> = {
[AppBarTab.Query]: { [AppBarTab.Query]: {
label: <Box display="flex"><SearchIcon /><Typography variant="button">Query</Typography></Box>, label: <Box display="flex"><SearchIcon /><Box ml={.5}/><Typography variant="button">Query</Typography></Box>,
path: "/query", path: "/query",
}, },
[AppBarTab.Manage]: { [AppBarTab.Manage]: {
label: <Box display="flex"><BuildIcon /><Typography variant="button">Manage</Typography></Box>, label: <Box display="flex"><BuildIcon /><Box ml={.5}/><Typography variant="button">Manage</Typography></Box>,
path: "/manage", path: "/manage",
}, },
[AppBarTab.Browse]: {
label: <Box display="flex"><InfoIcon /><Box ml={.5}/><Typography variant="button">Browse</Typography></Box>,
path: undefined,
},
} }
export function UserMenu(props: { export function UserMenu(props: {
@ -86,13 +92,17 @@ export default function AppBar(props: {
<Box flexGrow={1}> <Box flexGrow={1}>
{auth.user && <Tabs {auth.user && <Tabs
value={props.selectedTab} value={props.selectedTab}
onChange={(e: any, val: AppBarTab) => history.push(appBarTabProps[val].path)} onChange={(e: any, val: AppBarTab) => {
let path = appBarTabProps[val].path
path && history.push(appBarTabProps[val].path)
}}
variant="scrollable" variant="scrollable"
scrollButtons="auto" scrollButtons="auto"
> >
{Object.keys(appBarTabProps).map((tab: any, idx: number) => <MuiTab {Object.keys(appBarTabProps).map((tab: any, idx: number) => <MuiTab
label={appBarTabProps[tab].label} label={appBarTabProps[tab].label}
value={idx} value={idx}
disabled={!(appBarTabProps[tab].path) && idx !== props.selectedTab}
/>)} />)}
</Tabs>} </Tabs>}
</Box> </Box>

@ -13,15 +13,21 @@ export interface IProps {
whichStore: ExternalStore, 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, string> = {
[ExternalStore.GooglePlayMusic]: 'play.google.com',
[ExternalStore.Spotify]: 'spotify.com',
[ExternalStore.YoutubeMusic]: 'music.youtube.com',
}
export function whichStore(url: string) { export function whichStore(url: string) {
if (url.includes('play.google.com')) { return Object.keys(StoreURLIdentifiers).reduce((prev: string | undefined, cur: string) => {
return ExternalStore.GooglePlayMusic; if(url.includes(StoreURLIdentifiers[cur as ExternalStore])) {
} else if (url.includes('spotify.com')) { return cur;
return ExternalStore.Spotify; }
} else if (url.includes('music.youtube.com')) { return prev;
return ExternalStore.YoutubeMusic; }, undefined);
}
return undefined;
} }
export default function StoreLinkIcon(props: any) { export default function StoreLinkIcon(props: any) {

@ -49,22 +49,20 @@ export function AlbumWindowReducer(state: AlbumWindowState, action: any) {
} }
export async function getAlbumMetadata(id: number) { export async function getAlbumMetadata(id: number) {
return (await queryAlbums({ let result: any = await queryAlbums(
query: { {
a: QueryLeafBy.AlbumId, a: QueryLeafBy.AlbumId,
b: id, b: id,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, }, 0, 1, serverApi.QueryResponseType.Details
offset: 0, );
limit: 1, return result[0];
})
)[0];
} }
export default function AlbumWindow(props: {}) { export default function AlbumWindow(props: {}) {
const { id } = useParams(); const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(AlbumWindowReducer, { const [state, dispatch] = useReducer(AlbumWindowReducer, {
id: id, id: parseInt(id),
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
songGetters: songGetters, songGetters: songGetters,
@ -99,16 +97,14 @@ export function AlbumWindowControlled(props: {
if (songsOnAlbum) { return; } if (songsOnAlbum) { return; }
(async () => { (async () => {
const songs = await querySongs({ const songs = await querySongs(
query: { {
a: QueryLeafBy.AlbumId, a: QueryLeafBy.AlbumId,
b: albumId, b: albumId,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, }, 0, -1, serverApi.QueryResponseType.Details
offset: 0, )
limit: -1, .catch((e: any) => { handleNotLoggedIn(auth, e) });
})
.catch((e: any) => { handleNotLoggedIn(auth, e) });
dispatch({ dispatch({
type: AlbumWindowStateActions.SetSongs, type: AlbumWindowStateActions.SetSongs,
value: songs, value: songs,

@ -54,21 +54,20 @@ export interface IProps {
} }
export async function getArtistMetadata(id: number) { export async function getArtistMetadata(id: number) {
return (await queryArtists({ let response: any = await queryArtists(
query: { {
a: QueryLeafBy.ArtistId, a: QueryLeafBy.ArtistId,
b: id, b: id,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, }, 0, 1, serverApi.QueryResponseType.Details
offset: 0, );
limit: 1, return response[0];
}))[0];
} }
export default function ArtistWindow(props: {}) { export default function ArtistWindow(props: {}) {
const { id } = useParams(); const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(ArtistWindowReducer, { const [state, dispatch] = useReducer(ArtistWindowReducer, {
id: id, id: parseInt(id),
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
songGetters: songGetters, songGetters: songGetters,
@ -103,16 +102,14 @@ export function ArtistWindowControlled(props: {
if (songsByArtist) { return; } if (songsByArtist) { return; }
(async () => { (async () => {
const songs = await querySongs({ const songs = await querySongs(
query: { {
a: QueryLeafBy.ArtistId, a: QueryLeafBy.ArtistId,
b: artistId, b: artistId,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, }, 0, -1, serverApi.QueryResponseType.Details,
offset: 0, )
limit: -1, .catch((e: any) => { handleNotLoggedIn(auth, e) });
})
.catch((e: any) => { handleNotLoggedIn(auth, e) });
dispatch({ dispatch({
type: ArtistWindowStateActions.SetSongs, type: ArtistWindowStateActions.SetSongs,
value: songs, value: songs,

@ -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<Counts | undefined>(undefined);
let [linkedCounts, setLinkedCounts] = useState<Record<string, Counts>>({});
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<number> = queryStoreCount(s, ItemType.Song);
let albumsPromise: Promise<number> = queryStoreCount(s, ItemType.Album);
let artistsPromise: Promise<number> = queryStoreCount(s, ItemType.Artist);
let updatePromise = Promise.all([songsPromise, albumsPromise, artistsPromise]).then(
(r: any[]) => {
setLinkedCounts((prev: Record<string, Counts>) => {
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 <Box>
{totalCounts && storeReady(s) && <Box>
<Typography>
{s}:<br />
{linkedCounts[s].songs} / {totalCounts.songs} songs linked<br />
{linkedCounts[s].artists} / {totalCounts.artists} artists linked<br />
{linkedCounts[s].albums} / {totalCounts.albums} albums linked<br />
<br />
</Typography>
</Box>}
</Box>
})}
</>
}

@ -5,6 +5,8 @@ import { useHistory } from 'react-router';
import { useAuth, Auth } from '../../../lib/useAuth'; import { useAuth, Auth } from '../../../lib/useAuth';
import Alert from '@material-ui/lab/Alert'; import Alert from '@material-ui/lab/Alert';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import LinksStatusWidget from './LinksStatusWidget';
export interface ManageLinksWindowState extends WindowState { export interface ManageLinksWindowState extends WindowState {
dummy: boolean dummy: boolean
@ -34,5 +36,27 @@ export function ManageLinksWindowControlled(props: {
state: ManageLinksWindowState, state: ManageLinksWindowState,
dispatch: (action: any) => void, dispatch: (action: any) => void,
}) { }) {
return <>Hi!</>; return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="80%"
>
<OpenInNewIcon style={{ fontSize: 80 }} />
</Box>
<Box
m={1}
mt={4}
width="80%"
>
<Typography variant="h4">Manage Links</Typography>
</Box>
<Box
m={1}
mt={4}
width="80%"
>
<LinksStatusWidget/>
</Box>
</Box>;
} }

@ -13,6 +13,7 @@ import Alert from '@material-ui/lab/Alert';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { NotLoggedInError, handleNotLoggedIn } from '../../../lib/backend/request'; import { NotLoggedInError, handleNotLoggedIn } from '../../../lib/backend/request';
import { useAuth } from '../../../lib/useAuth'; import { useAuth } from '../../../lib/useAuth';
import * as serverApi from '../../../api';
var _ = require('lodash'); var _ = require('lodash');
export interface ManageTagsWindowState extends WindowState { export interface ManageTagsWindowState extends WindowState {
@ -79,11 +80,9 @@ export function organiseTags(allTags: Record<string, any>, fromId: string | null
export async function getAllTags() { export async function getAllTags() {
return (async () => { return (async () => {
var retval: Record<string, any> = {}; var retval: Record<string, any> = {};
const tags = await queryTags({ const tags: any = await queryTags(
query: undefined, undefined, 0, -1, serverApi.QueryResponseType.Details,
offset: 0, );
limit: -1,
});
// Convert numeric IDs to string IDs because that is // Convert numeric IDs to string IDs because that is
// what we work with within this component. // what we work with within this component.
tags.forEach((tag: any) => { tags.forEach((tag: any) => {
@ -426,13 +425,13 @@ export function ManageTagsWindowControlled(props: {
type: ManageTagsWindowActions.Reset type: ManageTagsWindowActions.Reset
}); });
}) })
.catch((e: any) => { handleNotLoggedIn(auth, e) }) .catch((e: any) => { handleNotLoggedIn(auth, e) })
.catch((e: Error) => { .catch((e: Error) => {
props.dispatch({ props.dispatch({
type: ManageTagsWindowActions.SetAlert, type: ManageTagsWindowActions.SetAlert,
value: <Alert severity="error">Failed to save changes: {e.message}</Alert>, value: <Alert severity="error">Failed to save changes: {e.message}</Alert>,
})
}) })
})
}} }}
getTagDetails={(id: string) => tagsWithChanges[id]} getTagDetails={(id: string) => tagsWithChanges[id]}
/> />

@ -6,6 +6,7 @@ import SongTable from '../../tables/ResultsTable';
import { songGetters } from '../../../lib/songGetters'; import { songGetters } from '../../../lib/songGetters';
import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries'; import { queryArtists, querySongs, queryAlbums, queryTags } from '../../../lib/backend/queries';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import { QueryResponseType } from '../../../api';
var _ = require('lodash'); var _ = require('lodash');
export interface ResultsForQuery { export interface ResultsForQuery {
@ -26,53 +27,51 @@ export enum QueryWindowStateActions {
} }
async function getArtistNames(filter: string) { async function getArtistNames(filter: string) {
const artists = await queryArtists({ const artists: any = await queryArtists(
query: filter.length > 0 ? { filter.length > 0 ? {
a: QueryLeafBy.ArtistName, a: QueryLeafBy.ArtistName,
b: '%' + filter + '%', b: '%' + filter + '%',
leafOp: QueryLeafOp.Like leafOp: QueryLeafOp.Like
} : undefined, } : undefined,
offset: 0, 0, -1, QueryResponseType.Details
limit: -1, );
});
return [...(new Set([...(artists.map((a: any) => a.name))]))]; return [...(new Set([...(artists.map((a: any) => a.name))]))];
} }
async function getAlbumNames(filter: string) { async function getAlbumNames(filter: string) {
const albums = await queryAlbums({ const albums: any = await queryAlbums(
query: filter.length > 0 ? { filter.length > 0 ? {
a: QueryLeafBy.AlbumName, a: QueryLeafBy.AlbumName,
b: '%' + filter + '%', b: '%' + filter + '%',
leafOp: QueryLeafOp.Like leafOp: QueryLeafOp.Like
} : undefined, } : undefined,
offset: 0, 0, -1, QueryResponseType.Details
limit: -1, );
});
return [...(new Set([...(albums.map((a: any) => a.name))]))]; return [...(new Set([...(albums.map((a: any) => a.name))]))];
} }
async function getSongTitles(filter: string) { async function getSongTitles(filter: string) {
const songs = await querySongs({ const songs: any = await querySongs(
query: filter.length > 0 ? { filter.length > 0 ? {
a: QueryLeafBy.SongTitle, a: QueryLeafBy.SongTitle,
b: '%' + filter + '%', b: '%' + filter + '%',
leafOp: QueryLeafOp.Like leafOp: QueryLeafOp.Like
} : undefined, } : undefined,
offset: 0, 0, -1, QueryResponseType.Details
limit: -1, );
});
return [...(new Set([...(songs.map((s: any) => s.title))]))]; return [...(new Set([...(songs.map((s: any) => s.title))]))];
} }
async function getTagItems() { async function getTagItems(): Promise<any> {
return await queryTags({ let tags: any = await queryTags(
query: undefined, undefined,
offset: 0, 0, -1, QueryResponseType.Details
limit: -1, );
});
return tags;
} }
export function QueryWindowReducer(state: QueryWindowState, action: any) { export function QueryWindowReducer(state: QueryWindowState, action: any) {
@ -112,17 +111,18 @@ export function QueryWindowControlled(props: {
} }
let setResultsForQuery = useCallback((r: ResultsForQuery | null) => { let setResultsForQuery = useCallback((r: ResultsForQuery | null) => {
dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r }); dispatch({ type: QueryWindowStateActions.SetResultsForQuery, value: r });
}, [ dispatch ]); }, [dispatch]);
const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query)); const loading = query && (!resultsFor || !_.isEqual(resultsFor.for, query));
const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : []; const showResults = (query && resultsFor && query === resultsFor.for) ? resultsFor.results : [];
const doQuery = useCallback(async (_query: QueryElem) => { const doQuery = useCallback(async (_query: QueryElem) => {
const songs = await querySongs({ const songs: any = await querySongs(
query: _query, _query,
offset: 0, 0,
limit: 100, //TODO: pagination 100, //TODO: pagination
}); QueryResponseType.Details
);
if (_.isEqual(query, _query)) { if (_.isEqual(query, _query)) {
setResultsForQuery({ setResultsForQuery({

@ -39,21 +39,20 @@ export function SongWindowReducer(state: SongWindowState, action: any) {
} }
export async function getSongMetadata(id: number) { export async function getSongMetadata(id: number) {
return (await querySongs({ let response: any = await querySongs(
query: { {
a: QueryLeafBy.SongId, a: QueryLeafBy.SongId,
b: id, b: id,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, }, 0, 1, serverApi.QueryResponseType.Details
offset: 0, );
limit: 1, return response[0];
}))[0];
} }
export default function SongWindow(props: {}) { export default function SongWindow(props: {}) {
const { id } = useParams(); const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(SongWindowReducer, { const [state, dispatch] = useReducer(SongWindowReducer, {
id: id, id: parseInt(id),
metadata: null, metadata: null,
}); });

@ -52,15 +52,15 @@ export function TagWindowReducer(state: TagWindowState, action: any) {
} }
export async function getTagMetadata(id: number) { export async function getTagMetadata(id: number) {
var tag = (await queryTags({ let tags: any = await queryTags(
query: { {
a: QueryLeafBy.TagId, a: QueryLeafBy.TagId,
b: id, b: id,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, }, 0, 1, serverApi.QueryResponseType.Details
offset: 0, );
limit: 1,
}))[0]; var tag = tags[0];
// Recursively fetch parent tags to build the full metadata. // Recursively fetch parent tags to build the full metadata.
if (tag.parentId) { if (tag.parentId) {
@ -76,9 +76,9 @@ export async function getTagMetadata(id: number) {
} }
export default function TagWindow(props: {}) { export default function TagWindow(props: {}) {
const { id } = useParams(); const { id } = useParams<{ id: string }>();
const [state, dispatch] = useReducer(TagWindowReducer, { const [state, dispatch] = useReducer(TagWindowReducer, {
id: id, id: parseInt(id),
metadata: null, metadata: null,
pendingChanges: null, pendingChanges: null,
songGetters: songGetters, songGetters: songGetters,
@ -113,15 +113,13 @@ export function TagWindowControlled(props: {
if (songsWithTag) { return; } if (songsWithTag) { return; }
(async () => { (async () => {
const songs = await querySongs({ const songs: any = await querySongs(
query: { {
a: QueryLeafBy.TagId, a: QueryLeafBy.TagId,
b: tagId, b: tagId,
leafOp: QueryLeafOp.Equals, leafOp: QueryLeafOp.Equals,
}, }, 0, -1, serverApi.QueryResponseType.Details,
offset: 0, );
limit: -1,
});
dispatch({ dispatch({
type: TagWindowStateActions.SetSongs, type: TagWindowStateActions.SetSongs,
value: songs, value: songs,

@ -2,18 +2,39 @@ import * as serverApi from '../../api';
import { QueryElem, toApiQuery } from '../query/Query'; import { QueryElem, toApiQuery } from '../query/Query';
import backendRequest from './request'; import backendRequest from './request';
export interface QueryArgs { export async function queryItems(
query?: QueryElem, types: serverApi.ItemType[],
offset: number, query: QueryElem | undefined,
limit: number, offset: number | undefined,
} limit: number | undefined,
responseType: serverApi.QueryResponseType,
export async function queryArtists(args: QueryArgs) { ): 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 = { var q: serverApi.QueryRequest = {
query: args.query ? toApiQuery(args.query) : {}, query: query ? toApiQuery(query) : {},
offsetsLimits: { offsetsLimits: {
artistOffset: args.offset, artistOffset: (serverApi.ItemType.Artist in types) ? (offset || 0) : undefined,
artistLimit: args.limit, 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: { ordering: {
orderBy: { orderBy: {
@ -21,6 +42,7 @@ export async function queryArtists(args: QueryArgs) {
}, },
ascending: true, ascending: true,
}, },
responseType: responseType,
}; };
const requestOpts = { const requestOpts = {
@ -32,110 +54,174 @@ export async function queryArtists(args: QueryArgs) {
return (async () => { return (async () => {
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
let json: any = await response.json(); let json: any = await response.json();
return json.artists; return json;
})(); })();
} }
export async function queryAlbums(args: QueryArgs) { export async function queryArtists(
var q: serverApi.QueryRequest = { query: QueryElem | undefined,
query: args.query ? toApiQuery(args.query) : {}, offset: number | undefined,
offsetsLimits: { limit: number | undefined,
albumOffset: args.offset, responseType: serverApi.QueryResponseType,
albumLimit: args.limit, ): Promise<serverApi.ArtistDetails[] | number[] | number> {
}, let r = await queryItems([serverApi.ItemType.Artist], query, offset, limit, responseType);
ordering: { return r.artists;
orderBy: {
type: serverApi.OrderByType.Name, // var q: serverApi.QueryRequest = {
}, // query: query ? toApiQuery(query) : {},
ascending: true, // offsetsLimits: {
}, // artistOffset: offset,
}; // artistLimit: limit,
// },
const requestOpts = { // ordering: {
method: 'POST', // orderBy: {
headers: { 'Content-Type': 'application/json' }, // type: serverApi.OrderByType.Name,
body: JSON.stringify(q), // },
}; // ascending: true,
// },
return (async () => { // responseType: responseType,
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) // };
let json: any = await response.json();
return json.albums; // 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) { export async function queryAlbums(
var q: serverApi.QueryRequest = { query: QueryElem | undefined,
query: args.query ? toApiQuery(args.query) : {}, offset: number | undefined,
offsetsLimits: { limit: number | undefined,
songOffset: args.offset, responseType: serverApi.QueryResponseType,
songLimit: args.limit, ): Promise<serverApi.AlbumDetails[] | number[] | number> {
}, let r = await queryItems([serverApi.ItemType.Album], query, offset, limit, responseType);
ordering: { return r.albums;
orderBy: {
type: serverApi.OrderByType.Name, // var q: serverApi.QueryRequest = {
}, // query: query ? toApiQuery(query) : {},
ascending: true, // offsetsLimits: {
}, // albumOffset: offset,
}; // albumLimit: limit,
// },
const requestOpts = { // ordering: {
method: 'POST', // orderBy: {
headers: { 'Content-Type': 'application/json' }, // type: serverApi.OrderByType.Name,
body: JSON.stringify(q), // },
}; // ascending: true,
// },
return (async () => { // responseType: responseType,
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts) // };
let json: any = await response.json();
return json.songs; // 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) { export async function querySongs(
var q: serverApi.QueryRequest = { query: QueryElem | undefined,
query: args.query ? toApiQuery(args.query) : {}, offset: number | undefined,
offsetsLimits: { limit: number | undefined,
tagOffset: args.offset, responseType: serverApi.QueryResponseType,
tagLimit: args.limit, ): Promise<serverApi.SongDetails[] | number[] | number> {
}, let r = await queryItems([serverApi.ItemType.Song], query, offset, limit, responseType);
ordering: { return r.songs;
orderBy: {
type: serverApi.OrderByType.Name, // var q: serverApi.QueryRequest = {
}, // query: query ? toApiQuery(query) : {},
ascending: true, // offsetsLimits: {
}, // songOffset: offset,
}; // songLimit: limit,
// },
const requestOpts = { // ordering: {
method: 'POST', // orderBy: {
headers: { 'Content-Type': 'application/json' }, // type: serverApi.OrderByType.Name,
body: JSON.stringify(q), // },
}; // 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 () => { export async function queryTags(
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts); query: QueryElem | undefined,
let json: any = await response.json(); offset: number | undefined,
const tags = json.tags; limit: number | undefined,
responseType: serverApi.QueryResponseType,
// Organise the tags into a tree structure. ): Promise<serverApi.TagDetails[] | number[] | number> {
// First, we put them in an indexed dict. let r = await queryItems([serverApi.ItemType.Tag], query, offset, limit, responseType);
const idxTags: Record<number, any> = {}; return r.tags;
tags.forEach((tag: any) => {
idxTags[tag.tagId] = { // var q: serverApi.QueryRequest = {
...tag, // query: query ? toApiQuery(query) : {},
childIds: [], // offsetsLimits: {
} // tagOffset: offset,
}) // tagLimit: limit,
// },
// Resolve children. // ordering: {
tags.forEach((tag: any) => { // orderBy: {
if(tag.parentId && tag.parentId in idxTags) { // type: serverApi.OrderByType.Name,
idxTags[tag.parentId].childIds.push(tag.tagId); // },
} // ascending: true,
}) // },
// responseType: responseType,
// Return the loose objects again. // };
return Object.values(idxTags);
})(); // 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<number, any> = {};
// 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);
// })();
} }

@ -10,7 +10,7 @@ export class NotLoggedInError extends Error {
} }
export function isNotLoggedInError(e: any): e is NotLoggedInError { 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<Response> { export default async function backendRequest(url: any, ...restArgs: any[]): Promise<Response> {
@ -23,6 +23,7 @@ export default async function backendRequest(url: any, ...restArgs: any[]): Prom
} }
export function handleNotLoggedIn(auth: Auth, e: any) { export function handleNotLoggedIn(auth: Auth, e: any) {
console.log("Error:", e);
if (isNotLoggedInError(e)) { if (isNotLoggedInError(e)) {
console.log("Not logged in!") console.log("Not logged in!")
auth.signout(); auth.signout();

@ -14,7 +14,7 @@ export type IntegrationState = {
}; };
export type IntegrationsState = IntegrationState[] | "Loading"; 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; return 'id' in v && 'integration' in v && 'properties' in v;
} }
@ -31,9 +31,9 @@ export const IntegrationClasses: Record<any, any> = {
[serverApi.IntegrationType.YoutubeWebScraper]: YoutubeMusicWebScraper, [serverApi.IntegrationType.YoutubeWebScraper]: YoutubeMusicWebScraper,
} }
export function makeDefaultIntegrationProperties(type: serverApi.IntegrationType): export function makeDefaultIntegrationProperties(type: serverApi.IntegrationType):
serverApi.CreateIntegrationRequest { serverApi.CreateIntegrationRequest {
switch(type) { switch (type) {
case serverApi.IntegrationType.SpotifyClientCredentials: { case serverApi.IntegrationType.SpotifyClientCredentials: {
return { return {
name: "Spotify App", name: "Spotify App",
@ -57,7 +57,7 @@ export function makeDefaultIntegrationProperties(type: serverApi.IntegrationType
} }
export function makeIntegration(p: serverApi.CreateIntegrationRequest, id: number) { export function makeIntegration(p: serverApi.CreateIntegrationRequest, id: number) {
switch(p.type) { switch (p.type) {
case serverApi.IntegrationType.SpotifyClientCredentials: { case serverApi.IntegrationType.SpotifyClientCredentials: {
return new SpotifyClientCreds(id); return new SpotifyClientCreds(id);
} }
@ -126,7 +126,7 @@ function useProvideIntegrations(): Integrations {
const [state, dispatch] = useReducer(IntegrationsReducer, []) const [state, dispatch] = useReducer(IntegrationsReducer, [])
let updateFromUpstream = async () => { let updateFromUpstream = async () => {
backend.getIntegrations() return await backend.getIntegrations()
.then((integrations: serverApi.ListIntegrationsResponse) => { .then((integrations: serverApi.ListIntegrationsResponse) => {
dispatch({ dispatch({
type: IntegrationsActions.Set, 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) => { 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(); await updateFromUpstream();
return id; return id;
} }
let deleteIntegration = async (id: number) => { let deleteIntegration = async (id: number) => {
await backend.deleteIntegration(id); await backend.deleteIntegration(id).catch((e: any) => { handleNotLoggedIn(auth, e) });
await updateFromUpstream(); await updateFromUpstream();
} }
let modifyIntegration = async (id: number, v: serverApi.CreateIntegrationRequest) => { 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(); await updateFromUpstream();
} }

@ -9,6 +9,9 @@ export enum QueryLeafBy {
TagId, TagId,
SongTitle, SongTitle,
SongId, SongId,
SongStoreLinks,
ArtistStoreLinks,
AlbumStoreLinks,
} }
export enum QueryLeafOp { export enum QueryLeafOp {
@ -174,6 +177,9 @@ export function toApiQuery(q: QueryElem) : serverApi.Query {
[QueryLeafBy.ArtistId]: serverApi.QueryElemProperty.artistId, [QueryLeafBy.ArtistId]: serverApi.QueryElemProperty.artistId,
[QueryLeafBy.TagId]: serverApi.QueryElemProperty.tagId, [QueryLeafBy.TagId]: serverApi.QueryElemProperty.tagId,
[QueryLeafBy.SongId]: serverApi.QueryElemProperty.songId, [QueryLeafBy.SongId]: serverApi.QueryElemProperty.songId,
[QueryLeafBy.SongStoreLinks]: serverApi.QueryElemProperty.songStoreLinks,
[QueryLeafBy.ArtistStoreLinks]: serverApi.QueryElemProperty.artistStoreLinks,
[QueryLeafBy.AlbumStoreLinks]: serverApi.QueryElemProperty.albumStoreLinks,
} }
const leafOpsMapping: any = { const leafOpsMapping: any = {
[QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq, [QueryLeafOp.Equals]: serverApi.QueryFilterOp.Eq,

@ -112,14 +112,15 @@ function useProvideAuth() {
}; };
const signout = () => { const signout = () => {
console.log("Signing out.");
setUser(null);
persistAuth(null);
return (async () => { return (async () => {
const url = (process.env.REACT_APP_BACKEND || "") + serverApi.LogoutEndpoint; const url = (process.env.REACT_APP_BACKEND || "") + serverApi.LogoutEndpoint;
const response = await fetch(url, { method: "POST" }); const response = await fetch(url, { method: "POST" });
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to log out."); throw new Error("Failed to log out.");
} }
setUser(null);
persistAuth(null);
})(); })();
}; };

Loading…
Cancel
Save