[mod edit: moved from the new userscript thread, was in response to @dvirtz Event Seeder update post here]
Nice, for the sub event part: Can you also seed the name with “pattern” appended?
“Parent Event Name” → "Main Event Name, "
“Parent Event Name, [WeekdayName/Day X]” → "Main Event Name, [Weekdayname/Day X]: "
Examples:
Vainstream Rockfest, Saturday: Main Stage
Vainstream Rockfest, Day 2: Main Stage
And also the type of the parent event.
PS: Since I have seen this to late, I had vibe coded a version (But I think yours is better and maintained):
// ==UserScript==
// @name MusicBrainz - Copy Event
// @namespace https://musicbrainz.org/
// @version 0.1
// @description Create a new event from an existing event using seeding
// @match https://musicbrainz.org/event/*
// @grant GM_openInTab
// ==/UserScript==
(function () {
'use strict';
if (!location.pathname.match(/^\/event\/[a-f0-9-]+/)) {
return;
}
function addSidebarButton() {
// Find sidebar section titled "Editing"
const headings = [...document.querySelectorAll('h2')];
const editingHeading = headings.find(
h => h.textContent.trim() === 'Editing'
);
if (!editingHeading) {
console.log('Copy Event: Editing section not found');
return;
}
// MusicBrainz sidebar layout:
// <h2>Editing</h2>
// <ul>...</ul>
const list = editingHeading.nextElementSibling;
if (!list || list.tagName !== 'UL') {
console.log('Copy Event: Editing list not found');
return;
}
const li = document.createElement('li');
const link = document.createElement('a');
link.href = '#';
link.textContent = 'Copy Event';
link.addEventListener('click', async (e) => {
e.preventDefault();
const opts = await showDialog();
if (!opts) return;
try {
const mbid =
location.pathname.match(
/\/event\/([a-f0-9-]+)/
)[1];
const data = await fetch(
`/ws/2/event/${mbid}?fmt=json&inc=artist-rels+event-rels+place-rels+series-rels`
).then(r => r.json());
const seed = buildSeed(data, opts);
GM_openInTab(seed, {
active: true
});
} catch (err) {
console.error(err);
alert(err.message);
}
});
li.appendChild(link);
list.appendChild(li);
}
function showDialog() {
const overlay = document.createElement('div');
overlay.style = `
position:fixed;
inset:0;
background:rgba(0,0,0,.35);
z-index:9999;
`;
const box = document.createElement('div');
box.style = `
background:white;
width:320px;
padding:16px;
border-radius:6px;
margin:100px auto;
`;
box.innerHTML = `
<h3>Copy Event</h3>
<label>
<input type="checkbox" id="series" checked>
Series relations
</label><br>
<label>
<input type="checkbox" id="place" checked>
Place relations
</label><br>
<label>
<input type="checkbox" id="artist">
Artist relations
</label><br>
<label>
<input type="checkbox" id="event">
Event relations
</label>
<br><br>
<button id="copy">Create</button>
<button id="cancel">Cancel</button>
`;
overlay.appendChild(box);
document.body.appendChild(overlay);
return new Promise(resolve => {
box.querySelector('#cancel').onclick = () => {
overlay.remove();
resolve(null);
};
box.querySelector('#copy').onclick = () => {
const result = {
series: box.querySelector('#series').checked,
place: box.querySelector('#place').checked,
artist: box.querySelector('#artist').checked,
event: box.querySelector('#event').checked
};
overlay.remove();
resolve(result);
};
});
}
const EVENT_TYPE_IDS = {
'Concert': 1,
'Festival': 2,
'Launch event': 3,
'Convention/Expo': 4,
'Masterclass/Clinic': 5,
'Stage performance': 6,
'Award ceremony': 7,
'Competition': 40
};
function buildSeed(event, opts) {
const params = new URLSearchParams();
// Basic fields
params.set(
'edit-event.name',
event.name || ''
);
if (event.disambiguation) {
params.set(
'edit-event.comment',
event.disambiguation
);
}
// Event type (MusicBrainz seed expects integer enum)
if (event.type) {
const typeId =
EVENT_TYPE_IDS[event.type];
if (typeId) {
params.set(
'edit-event.type_id',
String(typeId)
);
} else {
console.warn(
'Unknown event type:',
event.type
);
}
}
// Dates
const span = event['life-span'];
if (span?.begin) {
copyDate(
params,
'edit-event.period.begin_date',
span.begin
);
}
if (span?.end) {
copyDate(
params,
'edit-event.period.end_date',
span.end
);
}
// Optional edit note
params.set(
'edit-event.edit_note',
'Seed was automatically created'
);
// Relations
let relIndex = 0;
for (const rel of event.relations || []) {
if (
(rel.target_type === 'series' && !opts.series) ||
(rel.target_type === 'place' && !opts.place) ||
(rel.target_type === 'artist' && !opts.artist) ||
(rel.target_type === 'event' && !opts.event)
) {
continue;
}
addRelationship(
params,
rel,
relIndex++
);
}
return (
'https://musicbrainz.org/event/create?' +
params.toString()
);
}
function copyDate(
params,
prefix,
date
) {
const parts = date.split('-');
params.set(
`${prefix}.year`,
parts[0] || ''
);
params.set(
`${prefix}.month`,
parts[1] || ''
);
params.set(
`${prefix}.day`,
parts[2] || ''
);
}
function addRelationship(
params,
rel,
index
) {
const base =
`rels.${index}`;
// Relation type MBID
if (rel['type-id']) {
params.set(
`${base}.type`,
rel['type-id']
);
}
// Target entity lookup
const targetType =
rel['target-type'];
const target =
targetType
? rel[targetType]
: null;
if (!target?.id) {
console.warn(
'Missing target id',
rel
);
return;
}
params.set(
`${base}.target`,
target.id
);
// backward relations
if (rel.direction === 'backward') {
params.set(
`${base}.backward`,
'1'
);
}
// optional relation dates
if (rel.begin) {
copyDate(
params,
`${base}.period.begin_date`,
rel.begin
);
}
if (rel.end) {
copyDate(
params,
`${base}.period.end_date`,
rel.end
);
}
if (rel.ended) {
params.set(
`${base}.period.ended`,
'1'
);
}
}
addSidebarButton();
})();