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,
albums: AlbumDetails[] | number[] | number,
}
// Note: use -1 as an infinity limit.
export interface OffsetsLimits {
songOffset?: number,
songLimit?: number,

@ -73,28 +73,28 @@ export default function MainWindow(props: any) {
<QueryWindow />
</PrivateRoute>
<PrivateRoute path="/artist/:id">
<AppBar selectedTab={null} />
<AppBar selectedTab={AppBarTab.Browse} />
<ArtistWindow />
</PrivateRoute>
<PrivateRoute path="/tag/:id">
<AppBar selectedTab={null} />
<AppBar selectedTab={AppBarTab.Browse} />
<TagWindow />
</PrivateRoute>
<PrivateRoute path="/album/:id">
<AppBar selectedTab={null} />
<AppBar selectedTab={AppBarTab.Browse} />
<AlbumWindow />
</PrivateRoute>
<PrivateRoute path="/song/:id">
<AppBar selectedTab={null} />
<AppBar selectedTab={AppBarTab.Browse} />
<SongWindow />
</PrivateRoute>
<PrivateRoute path="/manage/tags">
<AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.Tags}/>
<ManageWindow selectedWindow={ManageWhat.Tags} />
</PrivateRoute>
<PrivateRoute path="/manage/links">
<AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.Links}/>
<ManageWindow selectedWindow={ManageWhat.Links} />
</PrivateRoute>
<PrivateRoute exact path="/manage">
<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 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<any, any> = {
[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",
},
[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",
},
[AppBarTab.Browse]: {
label: <Box display="flex"><InfoIcon /><Box ml={.5}/><Typography variant="button">Browse</Typography></Box>,
path: undefined,
},
}
export function UserMenu(props: {
@ -86,13 +92,17 @@ export default function AppBar(props: {
<Box flexGrow={1}>
{auth.user && <Tabs
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"
scrollButtons="auto"
>
{Object.keys(appBarTabProps).map((tab: any, idx: number) => <MuiTab
label={appBarTabProps[tab].label}
value={idx}
disabled={!(appBarTabProps[tab].path) && idx !== props.selectedTab}
/>)}
</Tabs>}
</Box>

@ -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, string> = {
[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) {

@ -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,

@ -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,

@ -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 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 <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 { 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<string, any>, fromId: string | null
export async function getAllTags() {
return (async () => {
var retval: Record<string, any> = {};
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: <Alert severity="error">Failed to save changes: {e.message}</Alert>,
.catch((e: any) => { handleNotLoggedIn(auth, e) })
.catch((e: Error) => {
props.dispatch({
type: ManageTagsWindowActions.SetAlert,
value: <Alert severity="error">Failed to save changes: {e.message}</Alert>,
})
})
})
}}
getTagDetails={(id: string) => tagsWithChanges[id]}
/>

@ -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<any> {
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({

@ -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,
});

@ -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,

@ -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<serverApi.ArtistDetails[] | number[] | number> {
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<serverApi.AlbumDetails[] | number[] | number> {
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<serverApi.SongDetails[] | number[] | number> {
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<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);
})();
export async function queryTags(
query: QueryElem | undefined,
offset: number | undefined,
limit: number | undefined,
responseType: serverApi.QueryResponseType,
): Promise<serverApi.TagDetails[] | number[] | number> {
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<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 {
return e.name === NotLoggedInError;
return e.name === "NotLoggedInError";
}
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) {
console.log("Error:", e);
if (isNotLoggedInError(e)) {
console.log("Not logged in!")
auth.signout();

@ -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<any, any> = {
[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();
}

@ -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,

@ -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);
})();
};

Loading…
Cancel
Save