diff --git a/rollup.config.mjs b/rollup.config.mjs index 3a30b87..1979d4d 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -19,7 +19,7 @@ export default defineConfig( minimize: false, extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], postcss: { - inject: name === 'acum-work-import', + inject: false, minimize: true, }, aliases: { @@ -38,7 +38,6 @@ export default defineConfig( 'solid-js/store': 'VM.solid.store', '@violentmonkey/ui': 'VM', }, - indent: false, }, })) ); diff --git a/src/acum-work-import/README.md b/src/acum-work-import/README.md index 66e5415..bb64349 100644 --- a/src/acum-work-import/README.md +++ b/src/acum-work-import/README.md @@ -7,12 +7,12 @@ This scripts allows you to import works from the database of the Israeli rights ![ACUM importer](assets/acum-work-import.png?raw=1) -To import works for a release: +To import a whole medium: -1. Open the release relationship editor -2. Find the album ID, this will be in the end of the album URL in ACUM, e.g. https://nocs.acum.org.il/acumsitesearchdb/album?albumid=011820 -3. Insert the ACUM album ID in the input box -4. Select the recordings whose works you want to import +1. Open the release relationship editor. +2. Find the album ID, this will be in the end of the album URL in ACUM, e.g. https://nocs.acum.org.il/acumsitesearchdb/album?albumid=011820. +3. Insert the ACUM album ID in the input box. +4. Select the recordings whose works you want to import. 5. Click the import button. 6. New works (green background links) will be created and exitsting works (yellow background) will be updated with links to the selected recordings and any writer and arranger as well as ISWCs and ACUM ID work attribute. ![updated works](assets/updated-works.png) @@ -23,6 +23,16 @@ To import works for a release: Since ACUM database treats every medium as a separate album all selected recordings should be under a single medium. +To import a single work: + +1. Open a work editor. +2. Find the work ID, this will be in the end of the work URL in ACUM, e.g. https://nocs.acum.org.il/acumsitesearchdb/work?workid=1005566. +3. Insert the ACUM work ID in the input box. +4. Click the import button. +5. The work will be updated with links writers, ISWCs and ACUM ID work attribute. +6. Review the changes. +7. Submit the work. + ## Release Notes See [CHANGELOG.md](CHANGELOG.md). diff --git a/src/acum-work-import/acum.ts b/src/acum-work-import/acum.ts index 5858ffe..dbe9c7a 100644 --- a/src/acum-work-import/acum.ts +++ b/src/acum-work-import/acum.ts @@ -72,7 +72,7 @@ type WorkInfoResponse = Response<{ workAlbums: ReadonlyArray; }>; -async function getWorkVersions(workId: string): Promise | undefined> { +export async function workVersions(workId: string): Promise | undefined> { const result = await tryFetchJSON( `https://nocs.acum.org.il/acumsitesearchdb/getworkinfo?workId=${workId}` ); @@ -93,6 +93,10 @@ function albumApiUrl(albumId: string) { return `https://nocs.acum.org.il/acumsitesearchdb/getalbuminfo?albumId=${albumId}`; } +export function workUrl(workId: string) { + return `https://nocs.acum.org.il/acumsitesearchdb/work?workId=${workId}`; +} + export async function getAlbumInfo(albumId: string): Promise { const result = await tryFetchJSON(albumApiUrl(albumId)); if (result) { @@ -107,7 +111,7 @@ export async function getAlbumInfo(albumId: string): Promise iswc.replace(/T(\d{3})(\d{3})(\d{3})(\d)/, 'T-$1.$2.$3-$4'); - return (await getWorkVersions(workID)) + return (await workVersions(workID)) ?.map(albumVersion => albumVersion.versionIswcNumber) .filter(iswc => iswc.length > 0) .map(formatISWC); diff --git a/src/acum-work-import/app.ts b/src/acum-work-import/app.ts index 7ed3be8..8c0df71 100644 --- a/src/acum-work-import/app.ts +++ b/src/acum-work-import/app.ts @@ -1,15 +1,24 @@ -import {createUI} from './ui/ui'; +import {createReleaseEditorUI, releaseEditorContainerId} from './ui/release-editor-ui'; +import {createWorkEditorUI} from './ui/work-editor-ui'; main(); function main() { VM.observe(document.body, () => { - const recordingCheckboxes = document.querySelectorAll( - 'input[type=checkbox].recording, input[type=checkbox].medium-recordings, input[type=checkbox].all-recordings' - ); - if (recordingCheckboxes.length > 0) { - createUI(recordingCheckboxes); - return true; + if (location.pathname.startsWith('/release/')) { + const recordingCheckboxes = document.querySelectorAll( + 'input[type=checkbox].recording, input[type=checkbox].medium-recordings, input[type=checkbox].all-recordings' + ); + if (recordingCheckboxes.length > 0 && !document.getElementById(releaseEditorContainerId)) { + createReleaseEditorUI(recordingCheckboxes); + return true; + } + } else { + const workForm = document.querySelector('form.edit-work'); + if (workForm) { + createWorkEditorUI(workForm); + return true; + } } }); } diff --git a/src/acum-work-import/artists.ts b/src/acum-work-import/artists.ts index 25fa750..ab1d5b5 100644 --- a/src/acum-work-import/artists.ts +++ b/src/acum-work-import/artists.ts @@ -1,5 +1,6 @@ +import {filter, from, lastValueFrom, mergeMap, tap} from 'rxjs'; import {tryFetchJSON} from 'src/common/musicbrainz/fetch'; -import {CreatorFull, Creators, IPBaseNumber} from './acum'; +import {Creator, CreatorFull, Creators, IPBaseNumber} from './acum'; import {AddWarning} from './ui/warnings'; function nameMatch(creator: CreatorFull, artist: ArtistSearchResultsT['artists'][number]): boolean { @@ -9,7 +10,7 @@ function nameMatch(creator: CreatorFull, artist: ArtistSearchResultsT['artists'] ); } -export async function findArtist( +async function findArtist( ipBaseNumber: IPBaseNumber, creators: Creators, addWarning: AddWarning @@ -35,3 +36,28 @@ export async function findArtist( return artistMBID ? await tryFetchJSON(`/ws/js/entity/${artistMBID}`) : null; } + +export async function linkArtists( + artistCache: Map>, + writers: readonly Creator[] | undefined, + creators: Creators, + doLink: (artist: ArtistT) => void, + addWarning: (message: string) => Set +) { + await lastValueFrom( + from(writers || []).pipe( + mergeMap( + async author => + await (artistCache.get(author.creatorIpBaseNumber) || + artistCache + .set(author.creatorIpBaseNumber, findArtist(author.creatorIpBaseNumber, creators, addWarning)) + .get(author.creatorIpBaseNumber)) + ), + filter((artist): artist is ArtistT => artist !== null), + tap(doLink) + ), + { + defaultValue: null, + } + ); +} diff --git a/src/acum-work-import/import-works.ts b/src/acum-work-import/import-album.ts similarity index 77% rename from src/acum-work-import/import-works.ts rename to src/acum-work-import/import-album.ts index 8a51bee..7ca363b 100644 --- a/src/acum-work-import/import-works.ts +++ b/src/acum-work-import/import-album.ts @@ -5,7 +5,6 @@ import { filter, from, ignoreElements, - isEmpty, lastValueFrom, map, merge, @@ -22,14 +21,14 @@ import {addEditNote} from 'src/common/musicbrainz/edit-note'; import {trackRecordingState} from 'src/common/musicbrainz/track-recording-state'; import {albumUrl, Creator, Creators, IPBaseNumber, searchName, WorkVersion} from './acum'; import {albumInfo} from './albums'; -import {findArtist} from './artists'; +import {linkArtists} from './artists'; import {addArrangerRelationship, addWriterRelationship} from './relationships'; import {AddWarning, ClearWarnings} from './ui/warnings'; import {workEditDataEqual} from './ui/work-edit-data'; import {WorkStateWithEditDataT} from './work-state'; import {addWork} from './works'; -export async function importWorks( +export async function importAlbum( albumId: string, addWarning: AddWarning, clearWarnings: ClearWarnings, @@ -46,36 +45,14 @@ export async function importWorks( // map of promises so that we don't fetch the same artist multiple times const artistCache = new Map>(); - const linkArtists = async ( - writers: ReadonlyArray | undefined, - creators: Creators, - doLink: (artist: ArtistT) => void, - addWarning: AddWarning - ) => { - await lastValueFrom( - from(writers || []).pipe( - mergeMap( - async author => - await (artistCache.get(author.creatorIpBaseNumber) || - artistCache - .set(author.creatorIpBaseNumber, findArtist(author.creatorIpBaseNumber, creators, addWarning)) - .get(author.creatorIpBaseNumber)) - ), - filter((artist): artist is ArtistT => artist !== null), - tap(doLink), - isEmpty() - ) - ); - }; - const linkWriters = async ( work: WorkT, writers: ReadonlyArray | undefined, creators: Creators, - linkTypeId: number, - addWarning: AddWarning + linkTypeId: number ) => { await linkArtists( + artistCache, writers, creators, (artist: ArtistT) => addWriterRelationship(work, artist, linkTypeId), @@ -86,10 +63,15 @@ export async function importWorks( const linkArrangers = async ( recording: RecordingT, arrangers: ReadonlyArray | undefined, - creators: Creators, - addWarning: AddWarning + creators: Creators ) => { - await linkArtists(arrangers, creators, (artist: ArtistT) => addArrangerRelationship(recording, artist), addWarning); + await linkArtists( + artistCache, + arrangers, + creators, + (artist: ArtistT) => addArrangerRelationship(recording, artist), + addWarning + ); }; const selectedRecordings = await lastValueFrom( @@ -108,17 +90,16 @@ export async function importWorks( ) ); - const linkCreators = async ([track, recording, workState, addWarning]: readonly [ + const linkCreators = async ([track, recording, workState]: readonly [ WorkVersion, RecordingT, WorkStateWithEditDataT, - AddWarning, ]): Promise => { const work = workState.work; - await linkWriters(work, track.authors, track.creators, LYRICIST_LINK_TYPE_ID, addWarning); - await linkWriters(work, track.composers, track.creators, COMPOSER_LINK_TYPE_ID, addWarning); - await linkWriters(work, track.translators, track.creators, TRANSLATOR_LINK_TYPE_ID, addWarning); - await linkArrangers(recording, track.arrangers, track.creators, addWarning); + await linkWriters(work, track.authors, track.creators, LYRICIST_LINK_TYPE_ID); + await linkWriters(work, track.composers, track.creators, COMPOSER_LINK_TYPE_ID); + await linkWriters(work, track.translators, track.creators, TRANSLATOR_LINK_TYPE_ID); + await linkArrangers(recording, track.arrangers, track.creators); return workState; }; @@ -152,7 +133,7 @@ export async function importWorks( }), mergeMap( async ([track, recordingState, addWarning]) => - [track, recordingState.recording, await addWork(track, recordingState, addWarning), addWarning] as const + [track, recordingState.recording, await addWork(track, recordingState, addWarning)] as const ), mergeMap(linkCreators), connect(shared => merge(shared.pipe(maybeSetEditNote), shared.pipe(updateProgress, ignoreElements()))) diff --git a/src/acum-work-import/import-work.ts b/src/acum-work-import/import-work.ts new file mode 100644 index 0000000..eaf3c46 --- /dev/null +++ b/src/acum-work-import/import-work.ts @@ -0,0 +1,135 @@ +import {from, lastValueFrom, switchMap, take, tap} from 'rxjs'; +import {asyncTap} from 'src/common/lib/asyncTap'; +import {COMPOSER_LINK_TYPE_ID, LYRICIST_LINK_TYPE_ID, TRANSLATOR_LINK_TYPE_ID} from 'src/common/musicbrainz/constants'; +import {addEditNote} from 'src/common/musicbrainz/edit-note'; +import {Creator, Creators, IPBaseNumber, workUrl, workVersions} from './acum'; +import {linkArtists} from './artists'; +import {addWriterRelationship} from './relationships'; +import {AddWarning} from './ui/warnings'; +import {workEditData} from './ui/work-edit-data'; +import {createWork} from './works'; + +export async function importWork(workId: string, form: HTMLFormElement, addWarning: AddWarning) { + // map of promises so that we don't fetch the same artist multiple times + const artistCache = new Map>(); + const work = + MB.relationshipEditor.state.entity.entityType == 'work' + ? MB.relationshipEditor.state.entity + : createWork({ + name: form.querySelector('[name="edit-work.name"]')?.getAttribute('value') || '', + }); + + const linkWriters = async ( + work: WorkT, + writers: ReadonlyArray | undefined, + creators: Creators, + linkTypeId: number + ) => { + await linkArtists( + artistCache, + writers, + creators, + (artist: ArtistT) => addWriterRelationship(work, artist, linkTypeId), + addWarning + ); + }; + + const versions = await workVersions(workId); + if (!versions) { + alert(`failed to find work ID ${workId}`); + throw new Error(`failed to find work ID ${workId}`); + } + + await lastValueFrom( + from(versions) + .pipe( + take(1), + switchMap(async track => { + const {editData} = await workEditData(work, track, addWarning); + return [track, editData] as const; + }), + tap(([, editData]) => setInput(form, 'name', editData.name, addWarning)), + tap(([, editData]) => setInput(form, 'comment', editData.comment, addWarning)), + tap(([, editData]) => setInput(form, 'type_id', String(editData.type_id), addWarning)), + asyncTap( + async ([, editData]) => + await ensureRowCount( + form.querySelector('#work-languages-editor')!, + '[name^="edit-work.languages."]', + editData.languages.length + ) + ), + tap(([, editData]) => + editData.languages.forEach((lang, index) => setInput(form, `languages.${index}`, String(lang), addWarning)) + ), + asyncTap( + async ([, editData]) => + await ensureRowCount( + form.querySelector('div.form-row-text-list')!, + '[name^="edit-work.iswcs."]', + editData.iswcs.length + ) + ), + tap(([, editData]) => + editData.iswcs.forEach((iswc, index) => setInput(form, `iswcs.${index}`, iswc, addWarning)) + ) + ) + .pipe( + asyncTap( + async ([, editData]) => + await ensureRowCount( + form.querySelector('#work-attributes')!, + '[name^="edit-work.attributes."]', + 2 * editData.attributes.length + ) + ), + tap(([, editData]) => { + editData.attributes.forEach((attr, index) => { + setInput(form, `attributes.${index}.type_id`, String(attr.type_id), addWarning); + setInput(form, `attributes.${index}.value`, attr.value, addWarning); + }); + }), + switchMap(async ([track]) => { + await linkWriters(work, track.authors, track.creators, LYRICIST_LINK_TYPE_ID); + await linkWriters(work, track.composers, track.creators, COMPOSER_LINK_TYPE_ID); + await linkWriters(work, track.translators, track.creators, TRANSLATOR_LINK_TYPE_ID); + return track; + }), + tap(() => addEditNote(`Imported from ${workUrl(workId)}`, form.ownerDocument)) + ) + ); +} + +function setInput(form: HTMLFormElement, name: string, value: string, addWarning: AddWarning) { + const input = form.querySelector(`[name="edit-work.${name}"]`); + if (input) { + if (input.value.trim() && input.value.trim() !== value.trim()) { + if (value.trim()) { + addWarning(`Suggeting different ${name.replace(/\W/g, ' ')}: ${value}`); + } + } else { + input.value = value; + input.dispatchEvent(new Event('change', {bubbles: true})); + } + } +} + +async function ensureRowCount(parent: HTMLElement, rowSelector: string, count: number) { + const rows = parent.querySelectorAll(rowSelector); + if (rows.length >= count) { + return; + } + const newRowBtn = parent.querySelector('button.add-item'); + for (let i = rows.length; i < count; i++) { + newRowBtn?.click(); + } + await new Promise(resolve => { + VM.observe(parent, (mutations, observer) => { + const rows = parent.querySelectorAll(rowSelector); + if (rows.length >= count) { + observer.disconnect(); + resolve(); + } + }); + }); +} diff --git a/src/acum-work-import/meta.ts b/src/acum-work-import/meta.ts index f4dcee3..5f28b2a 100644 --- a/src/acum-work-import/meta.ts +++ b/src/acum-work-import/meta.ts @@ -7,7 +7,9 @@ // @namespace https://github.com/dvirtz/musicbrainz-scripts // @downloadURL https://github.com/dvirtz/musicbrainz-scripts/releases/latest/download/acum-work-import.user.js // @supportURL https://github.com/dvirtz/musicbrainz-scripts/issues -// @match http*://*musicbrainz.org/release/*/edit-relationships +// @match http*://*.musicbrainz.org/release/*/edit-relationships +// @match http*://*.musicbrainz.org/work/* +// @match http*://*.musicbrainz.org/dialog?path=%2Fwork%2F* // @icon https://nocs.acum.org.il/acumsitesearchdb/resources/images/faviconSite.svg // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2 // @require https://cdn.jsdelivr.net/npm/@violentmonkey/ui@0.7 diff --git a/src/acum-work-import/ui/progressbar.tsx b/src/acum-work-import/ui/progressbar.tsx new file mode 100644 index 0000000..a4587d5 --- /dev/null +++ b/src/acum-work-import/ui/progressbar.tsx @@ -0,0 +1,23 @@ +import {Progress} from '@kobalte/core/progress'; +import {ComponentProps, Show, splitProps} from 'solid-js'; +import progressBarStyle from './progressbar.css'; +import styleInject from 'src/common/lib/styleInject'; + +styleInject(progressBarStyle); + +export function ProgressBar(props: ComponentProps & {label: string}) { + const [local, root] = splitProps(props, ['label']); + return ( + + +   +
+ {local.label} +  }> + + +
+
+
+ ); +} diff --git a/src/acum-work-import/ui/ui.tsx b/src/acum-work-import/ui/release-editor-ui.tsx similarity index 73% rename from src/acum-work-import/ui/ui.tsx rename to src/acum-work-import/ui/release-editor-ui.tsx index 78fd621..1093009 100644 --- a/src/acum-work-import/ui/ui.tsx +++ b/src/acum-work-import/ui/release-editor-ui.tsx @@ -1,15 +1,14 @@ import {Button} from '@kobalte/core/button'; -import {Progress} from '@kobalte/core/progress'; -import {ComponentProps, createEffect, createMemo, createSignal, on, Show, splitProps} from 'solid-js'; +import {createEffect, createMemo, createSignal, on} from 'solid-js'; import {render} from 'solid-js/web'; -import {releaseEditorTools} from 'src/common/musicbrainz/release-editor-tools'; -import {importWorks as tryImportWorks} from '../import-works'; +import {Toolbox} from 'src/common/musicbrainz/toolbox'; +import {importAlbum as tryImportWorks} from '../import-album'; import {submitWorks as trySubmitWorks} from '../submit'; -import {SelectionStatus, validateAlbumId, validateSelection} from '../validate'; -import './progressbar.css'; -import {useWarnings, Warnings, WarningsProvider} from './warnings'; +import {SelectionStatus, validateNumericId, validateSelection} from '../validate'; +import {ProgressBar} from './progressbar'; +import {useWarnings, WarningsProvider} from './warnings'; -void validateAlbumId; +void validateNumericId; function AcumImporter(props: {recordingCheckboxes: NodeListOf}) { const [albumId, setAlbumId] = createSignal('Album ID'); @@ -46,7 +45,7 @@ function AcumImporter(props: {recordingCheckboxes: NodeListOf} }) ); createEffect((prevTitle?: string) => { - const submitButton = document.querySelector('Button.submit') as HTMLButtonElement; + const submitButton = document.querySelector('button.submit') as HTMLButtonElement; submitButton.disabled = worksPending(); submitButton.title = worksPending() ? 'Submit works or cancel first' : (prevTitle ?? submitButton.title); return submitButton.title; @@ -79,6 +78,11 @@ function AcumImporter(props: {recordingCheckboxes: NodeListOf} clearWarnings(); } + addWarning( + "Only use this option after you've tried searching for the work(s) " + + 'you want to add, and are certain they do not already exist on MusicBrainz.' + ); + return ( <>
@@ -93,7 +97,7 @@ function AcumImporter(props: {recordingCheckboxes: NodeListOf} + +
+ + ); +} + +const releaseEditorContainerId = 'acum-work-import-container'; + +export function createWorkEditorUI(workForm: HTMLFormElement) { + if (workForm.querySelector(`#${releaseEditorContainerId}`)) { + return; + } + + const container = (
) as HTMLDivElement; + const inIframe = location.pathname.startsWith('/dialog'); + const toolbox = Toolbox(workForm.ownerDocument, inIframe ? 'iframe' : 'half-page'); + toolbox.append(container); + workForm.querySelector(inIframe ? '.half-width' : '.documentation')?.insertAdjacentElement('beforebegin', toolbox); + + render( + () => ( + + + + ), + container + ); +} diff --git a/src/acum-work-import/validate.ts b/src/acum-work-import/validate.ts index d4b5a85..0ff0682 100644 --- a/src/acum-work-import/validate.ts +++ b/src/acum-work-import/validate.ts @@ -12,21 +12,21 @@ declare module 'solid-js' { // eslint-disable-next-line @typescript-eslint/no-namespace namespace JSX { interface Directives { - validateAlbumId: [Signal, Setter]; // Corresponds to `use:validateAlbumId` + validateNumericId: [Signal, Setter]; // Corresponds to `use:validateAlbumId` } } } -export function validateAlbumId(input: HTMLInputElement, accessor: Accessor<[Signal, Setter]>) { - const [[albumId, setAlbumId], setAlbumIdValid] = accessor(); +export function validateNumericId(input: HTMLInputElement, accessor: Accessor<[Signal, Setter]>) { + const [[id, setId], setIdValid] = accessor(); input.oninput = () => { - setAlbumId(input.value); - if (/^\d+$/.test(albumId())) { - setAlbumIdValid(true); + setId(input.value); + if (/^\d+$/.test(id())) { + setIdValid(true); input.setCustomValidity(''); } else { - setAlbumIdValid(false); - input.setCustomValidity('Album ID must be a number'); + setIdValid(false); + input.setCustomValidity('ID must be a number'); } input.reportValidity(); }; diff --git a/src/acum-work-import/works.ts b/src/acum-work-import/works.ts index fc6b7a4..4cad8df 100644 --- a/src/acum-work-import/works.ts +++ b/src/acum-work-import/works.ts @@ -6,7 +6,7 @@ import {iterateRelationshipsInTargetTypeGroup} from 'src/common/musicbrainz/type import {WorkVersion} from './acum'; import {createRelationshipState} from './relationships'; import {AddWarning} from './ui/warnings'; -import {udpateEditData} from './ui/work-edit-data'; +import {workEditData} from './ui/work-edit-data'; import {addWorkEditor} from './ui/work-editor'; import {WorkStateWithEditDataT} from './work-state'; @@ -40,7 +40,7 @@ export async function addWork( } const workState = head(MB.tree.iterate(recordingState.relatedWorks)) as WorkStateWithEditDataT; - await udpateEditData(workState, track, addWarning); + Object.assign(workState, await workEditData(workState.work, track, addWarning)); addWorkEditor(workState, recordingState); return workState; } @@ -75,14 +75,14 @@ async function createNewWork(track: WorkVersion, recordingState: MediumRecording oldRelationshipState: null, }); // wait for the work to be added - const observer = await new Promise(resolve => { + await new Promise(resolve => { VM.observe(document.querySelector('.release-relationship-editor')!, (mutations, observer) => { if (document.querySelector(`.works a[href="#new-work-${newWork.id}"]`)) { - resolve(observer); + observer.disconnect(); + resolve(); } }); }); - observer.disconnect(); // refresh recording state const mediumRecordingStates = MB.tree.find( MB.relationshipEditor.state.mediums, @@ -100,7 +100,7 @@ async function createNewWork(track: WorkVersion, recordingState: MediumRecording )!; } -function createWork(attributes: Partial): WorkT { +export function createWork(attributes: Partial): WorkT { return MB.entity({ ...{ artists: [], diff --git a/src/common/lib/asyncTap.ts b/src/common/lib/asyncTap.ts new file mode 100644 index 0000000..d7e9b4d --- /dev/null +++ b/src/common/lib/asyncTap.ts @@ -0,0 +1,10 @@ +import {pipe, switchMap} from 'rxjs'; + +export function asyncTap(fn: (_: T) => Promise) { + return pipe( + switchMap(async (arg: T) => { + await fn(arg); + return arg; + }) + ); +} diff --git a/src/common/lib/styleInject.ts b/src/common/lib/styleInject.ts new file mode 100644 index 0000000..6aac6dc --- /dev/null +++ b/src/common/lib/styleInject.ts @@ -0,0 +1,32 @@ +// adapted from https://github.com/egoist/style-inject/blob/d093852c60f81f4860fcea2c770b70716af5703c/src/index.js + +export default function styleInject( + css: string, + options?: { + insertAt?: 'top' | 'bottom'; + document?: Document; + } +) { + options = Object.assign( + { + insertAt: 'bottom', + document: window.document, + }, + options + ); + if (!css || typeof options.document === 'undefined') return; + + const head = options.document.head || options.document.getElementsByTagName('head')[0]; + const style = options.document.createElement('style'); + style.appendChild(options.document.createTextNode(css)); + + if (options.insertAt === 'top') { + if (head.firstChild) { + head.insertBefore(style, head.firstChild); + } else { + head.appendChild(style); + } + } else { + head.appendChild(style); + } +} diff --git a/src/common/musicbrainz/constants.ts b/src/common/musicbrainz/constants.ts index b975654..b6fdff2 100644 --- a/src/common/musicbrainz/constants.ts +++ b/src/common/musicbrainz/constants.ts @@ -18,7 +18,6 @@ export const REL_STATUS_NOOP: RelationshipEditStatusT = 0; export const REL_STATUS_ADD: RelationshipEditStatusT = 1; export const REL_STATUS_EDIT: RelationshipEditStatusT = 2; export const REL_STATUS_REMOVE: RelationshipEditStatusT = 3; -export const ACUM_TYPE_ID = window.location.host.startsWith('test.') ? 141 : 206; export const LANGUAGE_MUL_ID = 284; export const LANGUAGE_ZXX_ID = 486; diff --git a/src/common/musicbrainz/edit-note.ts b/src/common/musicbrainz/edit-note.ts index de8af5f..bf88956 100644 --- a/src/common/musicbrainz/edit-note.ts +++ b/src/common/musicbrainz/edit-note.ts @@ -1,8 +1,14 @@ -export function addEditNote(message: string) { - const textArea = document.querySelector('#edit-note-text') as HTMLTextAreaElement; +export function addEditNote(message: string, document: Document = window.document) { + const textArea = document.querySelector('textarea.edit-note'); const note = editNote(message); - if (!textArea.value.includes(message)) { - MB.relationshipEditor.dispatch({type: 'update-edit-note', editNote: `${textArea.value}\n${note}`}); + if (textArea && !textArea?.value.includes(message)) { + const newNote = `${textArea?.value}\n${note}`; + if (MB.relationshipEditor.state.editNoteField) { + MB.relationshipEditor.dispatch({type: 'update-edit-note', editNote: newNote}); + } else { + textArea.value = newNote; + textArea.dispatchEvent(new Event('change', {bubbles: true})); + } } } diff --git a/src/common/musicbrainz/release-editor-tools.tsx b/src/common/musicbrainz/release-editor-tools.tsx deleted file mode 100644 index a95027f..0000000 --- a/src/common/musicbrainz/release-editor-tools.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export function releaseEditorTools(): HTMLDivElement { - const ID = 'dvirtz-release-editor-tools'; - const existing = document.getElementById(ID); - if (existing) { - return existing as HTMLDivElement; - } - - const toolbox = ( -
-

dvirtz MusicBrainz scripts

-
- ) as HTMLDivElement; - - document.querySelector('div.tabs')?.insertAdjacentElement('afterend', toolbox); - return toolbox; -} diff --git a/src/common/musicbrainz/toolbox.css b/src/common/musicbrainz/toolbox.css new file mode 100644 index 0000000..6961d35 --- /dev/null +++ b/src/common/musicbrainz/toolbox.css @@ -0,0 +1,20 @@ +#dvirtz-toolbox { + padding: 8px; + border: 5px dotted rgb(171, 171, 109); + margin: 0px -6px 6px; + + .warning { + border: 2px red dotted; + background: #FBE3E4; + padding: 0.5em; + font-weight: bold; + } +} + +#dvirtz-toolbox.half-page { + margin: 0px -6px 6px 550px; +} + +#dvirtz-toolbox.iframe { + margin: 0px -6px 6px 0px; +} diff --git a/src/common/musicbrainz/toolbox.tsx b/src/common/musicbrainz/toolbox.tsx new file mode 100644 index 0000000..b2e8555 --- /dev/null +++ b/src/common/musicbrainz/toolbox.tsx @@ -0,0 +1,18 @@ +import styleInject from '../lib/styleInject'; +import toolboxStyle from './toolbox.css'; + +export function Toolbox(doc: Document, className: 'full-page' | 'half-page' | 'iframe'): HTMLDivElement { + const ID = 'dvirtz-toolbox'; + const existing = doc.getElementById(ID); + if (existing) { + return existing as HTMLDivElement; + } + + styleInject(toolboxStyle, {document: doc}); + return ( +
+

dvirtz MusicBrainz scripts

+
+
+ ) as HTMLDivElement; +} diff --git a/src/common/musicbrainz/type-info.ts b/src/common/musicbrainz/type-info.ts new file mode 100644 index 0000000..19ca611 --- /dev/null +++ b/src/common/musicbrainz/type-info.ts @@ -0,0 +1,37 @@ +import PLazy from 'p-lazy'; +import {tryFetchJSON} from './fetch'; + +function byId(list: ReadonlyArray) { + return Object.fromEntries(list.map(item => [item.id, item])) as Record; +} + +function fetchTypeInfo(url: string, key: string) { + return PLazy.from(async () => byId((await tryFetchJSON<{[key: string]: T[]}>(url))?.[key] ?? [])); +} + +function fetchOrGetFromCache( + url: string, + key: string, + cacheKey?: K +) { + return PLazy.from(async () => + cacheKey && Object.keys(MB.linkedEntities[cacheKey]).length > 0 + ? MB.linkedEntities[cacheKey] + : fetchTypeInfo(url, key) + ); +} + +export const workAttributeTypes = fetchOrGetFromCache( + '/ws/js/type-info/work_attribute_type', + 'work_attribute_type_list', + 'work_attribute_type' +); + +export const workAttributeAllowedValues = fetchTypeInfo( + '/ws/js/type-info/work_attribute_type_allowed_value', + 'work_attribute_type_allowed_value_list' +); + +export const workTypes = fetchOrGetFromCache('/ws/js/type-info/work_type', 'work_type_list', 'work_type'); + +export const workLanguages = fetchOrGetFromCache('/ws/js/type-info/language', 'language_list', 'language');