|
|
|
@ -1,9 +1,16 @@ |
|
|
|
|
import React, { useState } from 'react'; |
|
|
|
|
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; |
|
|
|
|
import { Box, Button, Checkbox, createStyles, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControlLabel, List, ListItem, ListItemIcon, ListItemText, makeStyles, MenuItem, Paper, Select, Theme, Typography } from "@material-ui/core"; |
|
|
|
|
import StoreLinkIcon from '../../common/StoreLinkIcon'; |
|
|
|
|
import { $enum } from 'ts-enum-util'; |
|
|
|
|
import { IntegrationState, useIntegrations } from '../../../lib/integration/useIntegrations'; |
|
|
|
|
import { ExternalStore, IntegrationStores, IntegrationType } from '../../../api'; |
|
|
|
|
import { ExternalStore, IntegrationStores, IntegrationType, ItemType, QueryResponseType, StoreURLIdentifiers } from '../../../api'; |
|
|
|
|
import { start } from 'repl'; |
|
|
|
|
import { QueryLeafBy, QueryLeafOp, QueryNodeOp, queryNot } from '../../../lib/query/Query'; |
|
|
|
|
import { queryAlbums, queryArtists, queryItems, querySongs } from '../../../lib/backend/queries'; |
|
|
|
|
import asyncPool from "tiny-async-pool"; |
|
|
|
|
import { getSong } from '../../../lib/backend/songs'; |
|
|
|
|
import { getAlbum } from '../../../lib/backend/albums'; |
|
|
|
|
import { getArtist } from '../../../lib/backend/artists'; |
|
|
|
|
|
|
|
|
|
const useStyles = makeStyles((theme: Theme) => |
|
|
|
|
createStyles({ |
|
|
|
@ -13,6 +20,192 @@ const useStyles = makeStyles((theme: Theme) => |
|
|
|
|
}) |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
enum BatchJobState { |
|
|
|
|
Idle = 0, |
|
|
|
|
Collecting, |
|
|
|
|
Running, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface Task { |
|
|
|
|
itemType: ItemType, |
|
|
|
|
itemId: number, |
|
|
|
|
integrationId: number, |
|
|
|
|
store: ExternalStore, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
interface BatchJobStatus { |
|
|
|
|
state: BatchJobState, |
|
|
|
|
numTasks: number, |
|
|
|
|
tasksSuccess: number, |
|
|
|
|
tasksFailed: number, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function makeTasks( |
|
|
|
|
integration: IntegrationState, |
|
|
|
|
linkSongs: boolean, |
|
|
|
|
linkArtists: boolean, |
|
|
|
|
linkAlbums: boolean, |
|
|
|
|
addTaskCb: (t: Task) => void, |
|
|
|
|
) { |
|
|
|
|
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 maybeStore = integration.integration.providesStoreLink(); |
|
|
|
|
if (!maybeStore) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
let store = maybeStore as ExternalStore; |
|
|
|
|
let doForType = async (type: ItemType) => { |
|
|
|
|
let ids: number[] = ((await queryItems( |
|
|
|
|
[type], |
|
|
|
|
queryNot({ |
|
|
|
|
a: whichProp[type], |
|
|
|
|
leafOp: QueryLeafOp.Like, |
|
|
|
|
b: `%${StoreURLIdentifiers[store]}%`, |
|
|
|
|
}), |
|
|
|
|
undefined, |
|
|
|
|
undefined, |
|
|
|
|
QueryResponseType.Ids |
|
|
|
|
)) as any)[whichElem[type]]; |
|
|
|
|
ids.map((id: number) => { |
|
|
|
|
addTaskCb({ |
|
|
|
|
itemType: type, |
|
|
|
|
itemId: id, |
|
|
|
|
integrationId: integration.id, |
|
|
|
|
store: store, |
|
|
|
|
}); |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
var promises: Promise<any>[] = []; |
|
|
|
|
if (linkSongs) { promises.push(doForType(ItemType.Song)); } |
|
|
|
|
if (linkArtists) { promises.push(doForType(ItemType.Artist)); } |
|
|
|
|
if (linkAlbums) { promises.push(doForType(ItemType.Album)); } |
|
|
|
|
console.log("Awaiting answer...") |
|
|
|
|
await Promise.all(promises); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function doLinking( |
|
|
|
|
toLink: { integrationId: number, songs: boolean, artists: boolean, albums: boolean }[], |
|
|
|
|
setStatus: any, |
|
|
|
|
integrations: IntegrationState[], |
|
|
|
|
) { |
|
|
|
|
console.log("Linking start!", toLink); |
|
|
|
|
|
|
|
|
|
// Start the collecting phase.
|
|
|
|
|
setStatus({ |
|
|
|
|
state: BatchJobState.Collecting, |
|
|
|
|
numTasks: 0, |
|
|
|
|
tasksSuccess: 0, |
|
|
|
|
tasksFailed: 0, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
console.log("Starting collection"); |
|
|
|
|
var tasks: Task[] = []; |
|
|
|
|
|
|
|
|
|
let collectionPromises = toLink.map((v: any) => { |
|
|
|
|
let { integrationId, songs, artists, albums } = v; |
|
|
|
|
let integration = integrations.find((i: IntegrationState) => i.id === integrationId); |
|
|
|
|
if (!integration) { return; } |
|
|
|
|
console.log('integration collect:', integration) |
|
|
|
|
return makeTasks( |
|
|
|
|
integration, |
|
|
|
|
songs, |
|
|
|
|
artists, |
|
|
|
|
albums, |
|
|
|
|
(t: Task) => { tasks.push(t) } |
|
|
|
|
); |
|
|
|
|
}) |
|
|
|
|
console.log("Awaiting collection.") |
|
|
|
|
await Promise.all(collectionPromises); |
|
|
|
|
console.log("Done collecting.", tasks) |
|
|
|
|
// Start the linking phase.
|
|
|
|
|
setStatus((status: BatchJobStatus) => { |
|
|
|
|
status.state = BatchJobState.Running; |
|
|
|
|
status.numTasks = tasks.length; |
|
|
|
|
console.log("Collected status:", status) |
|
|
|
|
return status; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
let makeJob: (t: Task) => Promise<void> = (t: Task) => { |
|
|
|
|
let integration = integrations.find((i: IntegrationState) => i.id === t.integrationId); |
|
|
|
|
return (async () => { |
|
|
|
|
let onSuccess = () => setStatus((s: BatchJobStatus) => { s.tasksSuccess += 1; return s; }); |
|
|
|
|
let onFail = () => setStatus((s: BatchJobStatus) => { s.tasksFailed += 1; return s; }); |
|
|
|
|
try { |
|
|
|
|
if (integration === undefined) { return; } |
|
|
|
|
console.log('integration search:', integration) |
|
|
|
|
let _integration = integration as IntegrationState; |
|
|
|
|
let searchFuncs: any = { |
|
|
|
|
[ItemType.Song]: (q: any, l: any) => { return _integration.integration.searchSong(q, l) }, |
|
|
|
|
[ItemType.Album]: (q: any, l: any) => { return _integration.integration.searchAlbum(q, l) }, |
|
|
|
|
[ItemType.Artist]: (q: any, l: any) => { return _integration.integration.searchArtist(q, l) }, |
|
|
|
|
} |
|
|
|
|
// TODO include related items in search
|
|
|
|
|
let getFuncs: any = { |
|
|
|
|
[ItemType.Song]: getSong, |
|
|
|
|
[ItemType.Album]: getAlbum, |
|
|
|
|
[ItemType.Artist]: getArtist, |
|
|
|
|
} |
|
|
|
|
let queryFuncs: any = { |
|
|
|
|
[ItemType.Song]: (s: any) => `${s.title}`, |
|
|
|
|
[ItemType.Album]: (s: any) => `${s.name}`, |
|
|
|
|
[ItemType.Artist]: (s: any) => `${s.name}`, |
|
|
|
|
} |
|
|
|
|
let query = queryFuncs[t.itemType](await getFuncs[t.itemType](t.itemId)); |
|
|
|
|
let candidates = await searchFuncs[t.itemType]( |
|
|
|
|
query, |
|
|
|
|
1, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
console.log(query, candidates); |
|
|
|
|
if (candidates && candidates.length && candidates.length > 0) { |
|
|
|
|
onSuccess(); |
|
|
|
|
} else { |
|
|
|
|
onFail(); |
|
|
|
|
} |
|
|
|
|
} catch (e) { |
|
|
|
|
// Report fail
|
|
|
|
|
console.log("Error fetching candidates: ", e) |
|
|
|
|
onFail(); |
|
|
|
|
} |
|
|
|
|
})(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await asyncPool(4, tasks, makeJob); |
|
|
|
|
|
|
|
|
|
// Finalize.
|
|
|
|
|
setStatus((status: BatchJobStatus) => { |
|
|
|
|
status.state = BatchJobState.Idle; |
|
|
|
|
console.log("Done running:", status) |
|
|
|
|
return status; |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function ProgressDialog(props: { |
|
|
|
|
open: boolean, |
|
|
|
|
onClose: () => void, |
|
|
|
|
status: BatchJobStatus, |
|
|
|
|
}) { |
|
|
|
|
return <Dialog |
|
|
|
|
open={props.open} |
|
|
|
|
onClose={props.onClose} |
|
|
|
|
> |
|
|
|
|
<DialogTitle>Batch linking in progress...</DialogTitle> |
|
|
|
|
<DialogContent> |
|
|
|
|
<DialogContentText> |
|
|
|
|
Closing or refreshing this page will interrupt and abort the process. |
|
|
|
|
</DialogContentText> |
|
|
|
|
</DialogContent> |
|
|
|
|
</Dialog> |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function ConfirmDialog(props: { |
|
|
|
|
open: boolean |
|
|
|
|
onConfirm: () => void, |
|
|
|
@ -42,6 +235,12 @@ export default function BatchLinkDialog(props: { |
|
|
|
|
let integrations = useIntegrations(); |
|
|
|
|
let classes = useStyles(); |
|
|
|
|
let [confirmDialogOpen, setConfirmDialogOpen] = useState<boolean>(false); |
|
|
|
|
let [jobStatus, setJobStatus] = useState<BatchJobStatus>({ |
|
|
|
|
state: BatchJobState.Idle, |
|
|
|
|
numTasks: 0, |
|
|
|
|
tasksSuccess: 0, |
|
|
|
|
tasksFailed: 0, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
var compatibleIntegrations: Record<ExternalStore, IntegrationState[]> = { |
|
|
|
|
[ExternalStore.GooglePlayMusic]: [], |
|
|
|
@ -157,7 +356,33 @@ export default function BatchLinkDialog(props: { |
|
|
|
|
<ConfirmDialog |
|
|
|
|
open={confirmDialogOpen} |
|
|
|
|
onClose={() => setConfirmDialogOpen(false)} |
|
|
|
|
onConfirm={() => { }} |
|
|
|
|
onConfirm={() => { |
|
|
|
|
var toLink: any[] = []; |
|
|
|
|
Object.keys(storeSettings).forEach((store: string) => { |
|
|
|
|
let s = store as ExternalStore; |
|
|
|
|
let active = Boolean(compatibleIntegrations[s].length); |
|
|
|
|
|
|
|
|
|
if (active && storeSettings[s].selectedIntegration !== undefined) { |
|
|
|
|
toLink.push({ |
|
|
|
|
integrationId: compatibleIntegrations[s][storeSettings[s].selectedIntegration || 0].id, |
|
|
|
|
songs: storeSettings[s].linkSongs, |
|
|
|
|
artists: storeSettings[s].linkArtists, |
|
|
|
|
albums: storeSettings[s].linkAlbums, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
doLinking( |
|
|
|
|
toLink, |
|
|
|
|
setJobStatus, |
|
|
|
|
integrations.state === "Loading" ? |
|
|
|
|
[] : integrations.state, |
|
|
|
|
) |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
<ProgressDialog |
|
|
|
|
open={jobStatus.state === BatchJobState.Collecting || jobStatus.state === BatchJobState.Running} |
|
|
|
|
onClose={() => { }} |
|
|
|
|
status={jobStatus} |
|
|
|
|
/> |
|
|
|
|
</> |
|
|
|
|
} |