Skip to content

Commit

Permalink
feat: allow importing a single work from releaes editor
Browse files Browse the repository at this point in the history
enabled when there's a single track / selected recording

Fixes #48
  • Loading branch information
dvirtz committed Jan 9, 2025
1 parent 4315c44 commit 94be6c9
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 43 deletions.
17 changes: 13 additions & 4 deletions src/acum-work-import/acum.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {tryFetchJSON} from 'src/common/lib/fetch';

export const enum Entity {
Album = 'album',
Work = 'work',
}

export type IPBaseNumber = string;

type Bean<Type extends string> = {
Expand Down Expand Up @@ -162,16 +167,20 @@ export function workLanguage(track: WorkBean): WorkLanguage {
return stringToEnum(track.workLanguage, WorkLanguage);
}

export function replaceUrlWith(field: string): (input: string) => string {
export function replaceUrlWith(entities: Entity[]): (input: string) => [string, Entity | undefined] {
return (input: string) => {
try {
const url = new URL(input);
if (url.hostname === 'nocs.acum.org.il' && url.searchParams.has(field)) {
return url.searchParams.get(field)!;
if (url.hostname === 'nocs.acum.org.il') {
return (
entities
.map(entity => [url.searchParams.get(`${entity}id`), entity] as const)
.find((pair): pair is [string, Entity] => !!pair[0]) ?? [input, undefined]
);
}
} catch (e) {
console.debug('failed to parse URL', input, e);
}
return input;
return [input, undefined] as const;
};
}
50 changes: 30 additions & 20 deletions src/acum-work-import/import-album.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {compareInsensitive} from 'src/common/lib/compare';
import {head} from 'src/common/lib/head';
import {addEditNote} from 'src/common/musicbrainz/edit-note';
import {trackRecordingState} from 'src/common/musicbrainz/track-recording-state';
import {albumUrl, Creator, Creators, IPBaseNumber, trackName, WorkVersion} from './acum';
import {albumUrl, Creator, Creators, Entity, fetchWork, IPBaseNumber, trackName, WorkBean} from './acum';
import {albumInfo} from './albums';
import {linkArtists} from './artists';
import {addArrangerRelationship, addWriterRelationship} from './relationships';
Expand All @@ -31,16 +31,14 @@ import {WorkStateWithEditDataT} from './work-state';
import {addWork, linkWriters} from './works';

export async function importAlbum(
albumId: string,
entityId: string,
entity: Entity,
addWarning: AddWarning,
setProgress: Setter<readonly [number, string]>
): Promise<boolean> {
setProgress([0, 'Loading album info']);

const addTrackWarning = (track: WorkVersion) => (warning: string) =>
addWarning(`Track ${track.albumTrackNumber}: ${warning}`);

const albumBean = await albumInfo(albumId);
const addTrackWarning = (position: number) => (warning: string) => addWarning(`Track ${position}: ${warning}`);

// map of promises so that we don't fetch the same artist multiple times
const artistCache = new Map<IPBaseNumber, Promise<ArtistT | null>>();
Expand All @@ -60,7 +58,7 @@ export async function importAlbum(
);
};

const noSelection = MB.relationshipEditor.state.selectedRecordings?.size === 0;
const noSelection = (MB.relationshipEditor.state.selectedRecordings?.size ?? 0) === 0;

const selectedMediums = new Set(
noSelection
Expand All @@ -77,12 +75,23 @@ export async function importAlbum(
addWarning('select at least one recording');
return false;
case 1:
if (
entity == Entity.Work &&
MB.relationshipEditor.state.selectedRecordings?.size !== 1 &&
selectedMediums.size !== 1
) {
addWarning('select exactly one recording');
return false;
}
break;
default:
addWarning('select recordings only from a single medium');
return false;
}

const tracks: ReadonlyArray<WorkBean> =
entity == Entity.Work ? ((await fetchWork(entityId)) ?? []) : (await albumInfo(entityId)).tracks;

const selectedRecordings = await lastValueFrom(
of(head(selectedMediums.values())).pipe(
filter(
Expand All @@ -91,25 +100,26 @@ export async function importAlbum(
),
mergeMap(([medium, recordingStateTree]) => {
return zip(
from(albumBean.tracks),
from(medium.tracks?.map(track => track.position) ?? []),
from(tracks),
from(medium.tracks!.map(track => trackRecordingState(track, recordingStateTree)))
);
}),
filter((trackAndRecordingState): trackAndRecordingState is [WorkVersion, MediumRecordingStateT] => {
const [, recordingState] = trackAndRecordingState;
filter((trackAndRecordingState): trackAndRecordingState is [number, WorkBean, MediumRecordingStateT] => {
const [, , recordingState] = trackAndRecordingState;
return recordingState != null && (noSelection || recordingState.isSelected);
}),
toArray()
)
);

const linkCreators = async ([track, recording, workState]: readonly [
WorkVersion,
RecordingT,
WorkStateWithEditDataT,
]): Promise<WorkStateWithEditDataT> => {
const linkCreators = async (
track: WorkBean,
recording: RecordingT,
workState: WorkStateWithEditDataT,
addWarning: AddWarning
): Promise<WorkStateWithEditDataT> => {
const work = workState.work;
const addWarning = addTrackWarning(track);
await linkWriters(
artistCache,
track,
Expand All @@ -125,7 +135,7 @@ export async function importAlbum(
map(editedCount => editedCount > 0),
tap(hasEdits => {
if (hasEdits) {
addEditNote(`Imported from ${albumUrl(albumId)}`);
addEditNote(`Imported from ${albumUrl(entityId)}`);
} else {
addWarning('All works are up to date');
}
Expand All @@ -141,7 +151,7 @@ export async function importAlbum(

return await lastValueFrom(
from(selectedRecordings).pipe(
map(([track, recordingState]) => [track, recordingState, addTrackWarning(track)] as const),
map(([position, track, recordingState]) => [track, recordingState, addTrackWarning(position)] as const),
tap(([track, recordingState, addWarning]) => {
const recording = recordingState.recording;
if (trackName(track) != recording.name) {
Expand All @@ -154,9 +164,9 @@ export async function importAlbum(
}),
mergeMap(
async ([track, recordingState, addWarning]) =>
[track, recordingState.recording, await addWork(track, recordingState, addWarning)] as const
[track, recordingState.recording, await addWork(track, recordingState, addWarning), addWarning] as const
),
mergeMap(linkCreators),
mergeMap(args => linkCreators(...args)),
connect(shared => merge(shared.pipe(maybeSetEditNote), shared.pipe(updateProgress, ignoreElements())))
)
);
Expand Down
29 changes: 19 additions & 10 deletions src/acum-work-import/ui/import-form.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import {Button} from '@kobalte/core/button';
import {TextField} from '@kobalte/core/text-field';
import {createEffect, createSignal, ParentProps} from 'solid-js';
import {replaceUrlWith} from '../acum';
import {Entity, replaceUrlWith} from '../acum';

export function ImportForm(
props: ParentProps & {field: string; onSubmit: (id: string) => Promise<void>; idPattern: string}
props: ParentProps & {
entities: Entity[];
onSubmit: (id: string, entity: Entity) => Promise<void>;
idPattern: string;
}
) {
const [id, setId] = createSignal('');
const [entity, setEntity] = createSignal(props.entities[0]);

const [importing, setImporting] = createSignal(false);

let submitButton: HTMLButtonElement;
Expand All @@ -22,11 +28,19 @@ export function ImportForm(
ev.preventDefault();
setImporting(true);
props
.onSubmit(id())
.onSubmit(id(), entity())
.catch(console.error)
.finally(() => setImporting(false));
};

const onInput = (value: string) => {
const [id, entity] = replaceUrlWith(props.entities)(value);
setId(id);
if (entity) {
setEntity(entity);
}
};

return (
<form onSubmit={onSubmit}>
<div class="buttons" style={{display: 'flex'}}>
Expand All @@ -38,15 +52,10 @@ export function ImportForm(
></img>
<span>Import works from ACUM</span>
</Button>
<TextField
required={true}
value={id()}
onChange={value => setId(replaceUrlWith(`${props.field}id`)(value))}
style={{'margin': '0 7px 0 0'}}
>
<TextField required={true} value={id()} onChange={onInput} style={{'margin': '0 7px 0 0'}}>
<TextField.Input
pattern={props.idPattern}
placeholder={`${props.field.charAt(0).toUpperCase()}${props.field.slice(1)} ID`}
placeholder={`${props.entities.map(entity => `${entity.charAt(0).toUpperCase()}${entity.slice(1)}`).join('/')} ID`}
/>
</TextField>
{props.children}
Expand Down
7 changes: 4 additions & 3 deletions src/acum-work-import/ui/release-editor-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Button} from '@kobalte/core/button';
import {createEffect, createMemo, createSignal} from 'solid-js';
import {render} from 'solid-js/web';
import {Toolbox} from 'src/common/musicbrainz/toolbox';
import {Entity} from '../acum';
import {importAlbum as tryImportWorks} from '../import-album';
import {submitWorks as trySubmitWorks} from '../submit';
import {ImportForm} from './import-form';
Expand All @@ -22,10 +23,10 @@ function AcumImporter() {
return submitButton.title;
});

async function importWorks(albumId: string) {
async function importWorks(entityId: string, entity: Entity) {
clearWarnings();
try {
await tryImportWorks(albumId, addWarning, setProgress);
await tryImportWorks(entityId, entity, addWarning, setProgress);
setWorksPending(true);
} catch (err) {
console.error(err);
Expand Down Expand Up @@ -57,7 +58,7 @@ function AcumImporter() {

return (
<>
<ImportForm field="album" onSubmit={importWorks} idPattern="\d+">
<ImportForm entities={[Entity.Album, Entity.Work]} onSubmit={importWorks} idPattern="\d+">
<Button id="acum-work-submit" class="worksubmit" disabled={submissionDisabled()} onclick={submitWorks}>
<span>Submit works</span>
</Button>
Expand Down
4 changes: 2 additions & 2 deletions src/acum-work-import/ui/work-edit-data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {mergeArrays} from 'src/common/lib/merge-arrays';
import {LANGUAGE_ZXX_ID} from 'src/common/musicbrainz/constants';
import {fetchEditParams, urlFromMbid} from 'src/common/musicbrainz/edits';
import {workAttributeTypes, workLanguages, workTypes} from 'src/common/musicbrainz/type-info';
import {essenceType, EssenceType, trackName, WorkBean, workISWCs, workLanguage, WorkLanguage} from '../acum';
import {essenceType, EssenceType, WorkBean, workISWCs, workLanguage, WorkLanguage} from '../acum';
import {WorkStateWithEditDataT} from '../work-state';
import {AddWarning} from './warnings';

Expand Down Expand Up @@ -73,7 +73,7 @@ export async function workEditData(
return {
originalEditData,
editData: {
name: trackName(track),
name: originalEditData.name,
comment: originalEditData.comment,
type_id: [EssenceType.Song, EssenceType.ChoirSong].includes(essenceType(track))
? (Object.values(await workTypes).find(workType => workType.name === 'Song')?.id ?? null)
Expand Down
3 changes: 2 additions & 1 deletion src/acum-work-import/ui/work-editor-ui.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {render} from 'solid-js/web';
import {Toolbox} from 'src/common/musicbrainz/toolbox';
import {Entity} from '../acum';
import {importWork as tryImportWork} from '../import-work';
import {ImportForm} from './import-form';
import {useWarnings, WarningsProvider} from './warnings';
Expand All @@ -17,7 +18,7 @@ function AcumImporter(props: {form: HTMLFormElement}) {
}
}

return <ImportForm field="work" onSubmit={importWork} idPattern="[12][0-9A-Z]+" />;
return <ImportForm entities={[Entity.Work]} onSubmit={importWork} idPattern="[12][0-9A-Z]+" />;
}

const releaseEditorContainerId = 'acum-work-import-container';
Expand Down
6 changes: 3 additions & 3 deletions src/acum-work-import/works.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
TRANSLATOR_LINK_TYPE_ID,
} from 'src/common/musicbrainz/constants';
import {iterateRelationshipsInTargetTypeGroup} from 'src/common/musicbrainz/type-group';
import {trackName, WorkBean, WorkVersion} from './acum';
import {trackName, WorkBean} from './acum';
import {linkArtists} from './artists';
import {createRelationshipState} from './relationships';
import {AddWarning} from './ui/warnings';
Expand Down Expand Up @@ -39,7 +39,7 @@ function shouldAddNewWork(relatedWorks: MediumWorkStateTreeT) {
}

export async function addWork(
track: WorkVersion,
track: WorkBean,
recordingState: MediumRecordingStateT,
addWarning: AddWarning
): Promise<WorkStateWithEditDataT> {
Expand All @@ -53,7 +53,7 @@ export async function addWork(
return workState;
}

async function createNewWork(track: WorkVersion, recordingState: MediumRecordingStateT) {
async function createNewWork(track: WorkBean, recordingState: MediumRecordingStateT) {
const newWork = (() => {
if (workCache.has(track.fullWorkId)) {
return workCache.get(track.fullWorkId)!;
Expand Down

0 comments on commit 94be6c9

Please sign in to comment.