Skip to content

Instantly share code, notes, and snippets.

@Hans5958
Last active August 17, 2026 14:41
Show Gist options
  • Select an option

  • Save Hans5958/69126992963f37f0ee0bde9760df1262 to your computer and use it in GitHub Desktop.

Select an option

Save Hans5958/69126992963f37f0ee0bde9760df1262 to your computer and use it in GitHub Desktop.
VocaDB Name Filler: Fills VocaDB/UtaiteDB/TouhouDB song name and shows the aliases and the artists from the linked original entry. Licensed under MIT. More info: https://gitlab.com/Hans5958-MWS/vocadb-docs/-/wikis/notes/web-scripts#name-filler
// Original: Fills name and shows names and artists. Requires manual settings to keep it.
// Bookmarklet: javascript:(async()=>{const e=document.querySelector("a.btn-nomargin:nth-child(1)").href.split("S/")[1],t="nf:"+e,n=sessionStorage.getItem(t),l=n?.includes("{")?JSON.parse(n):await fetch(`//${location.host}/api/songs/${e}?fields=Artists,Names&lang=${await(cookieStore.get("UserSettings.LanguagePreference")?.then(e=>e?.value))||"English"}`).then(e=>e.json());sessionStorage.setItem(t,JSON.stringify(l));const o="div.editor-field > table > tbody",a=e=>document.querySelector(o+`> tr:nth-child(${e}) input:nth-child(1)`),i=e=>document.querySelector(o+`> tr:nth-child(${e}) > td`),r=e=>document.querySelector(`div.editor-field:nth-child(2) > select > option:nth-child(${e})`),s=["Japanese","Romaji","English"];for(let e=0;e<s.length;e++){const t=s[e],n=l.names.filter(e=>t===e.language)[0]?.value||"",o=a(e+1),d=o.parentElement;d.style.display="flex",d.style.gap=".5rem";const c="nf-original-primary-names";for(const e of d.querySelectorAll("."+c))d.removeChild(e);const m=document.createElement("ul");m.classList.add(c);const u=document.createElement("span");u.style.opacity="50%",u.textContent="empty";const p=document.createElement("li");p.textContent="Old: "+o.value,o.value||p.appendChild(u.cloneNode(!0));const h=document.createElement("li");h.textContent="New: "+n,n||h.appendChild(u.cloneNode(!0)),m.appendChild(p),m.appendChild(h),d.appendChild(m),o.value=n,l.defaultNameLanguage===t&&(console.log(i(e+1)),i(e+1).style.fontWeight="600 !important",r(e+2)&&(r(e+2).style.fontWeight="600"))}const d=l.names.filter(e=>"Unspecified"===e.language).map(e=>e.value),c=document.querySelector("div.editor-field:nth-child(4) > span:nth-child(2)");c&&!c.querySelector("br")&&(c.innerHTML+="<br>"+d.join("<br>"));const m=document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)"),u=document.createElement("ul");u.id="nf-artists";for(const e of m.querySelectorAll("#"+u.id))m.removeChild(e);l.artists.forEach(e=>{const t=document.createElement("li"),n=e.effectiveRoles,l="Default"===n?"D: "+e.artist?.artistType:n,o=e.isSupport?", Support":"",a=e.name+" ("+l+o+")";if(e.isCustomName)t.appendChild(document.createTextNode(a));else{let n=document.createElement("a");e.isCustomName||(n.href="/Ar/"+e.artist.id),n.textContent=a,t.appendChild(n)}u.appendChild(t)}),m.appendChild(u)})();
(async () => {
// Fetching and caching
const originalId = document.querySelector("a.btn-nomargin:nth-child(1)").href.split("S/")[1]
const cacheKey = "nf:" + originalId
const cachedRaw = sessionStorage.getItem(cacheKey)
const data = cachedRaw?.includes('{') ? JSON.parse(cachedRaw) : await fetch(`//${location.host}/api/songs/${originalId}?fields=Artists,Names&lang=${await cookieStore.get("UserSettings.LanguagePreference")?.then(c => c?.value) || 'English'}`).then((res) => res.json())
sessionStorage.setItem(cacheKey, JSON.stringify(data))
// Filling names; showing main language and aliases
const sameElStr = "div.editor-field > table > tbody"
const valueEl = i => document.querySelector(sameElStr + `> tr:nth-child(${i}) input:nth-child(1)`)
const keyEl = i => document.querySelector(sameElStr + `> tr:nth-child(${i}) > td`)
const choiceEl = i => document.querySelector(`div.editor-field:nth-child(2) > select > option:nth-child(${i})`)
const langs = ['Japanese', 'Romaji', 'English']
for (let i = 0; i < langs.length; i++) {
const lang = langs[i]
const newValue = data.names.filter((el) => lang === el.language)[0]?.value || ''
const inputEl = valueEl(i + 1)
const tdEl = inputEl.parentElement
tdEl.style.display = 'flex'
tdEl.style.gap = '.5rem'
const ulClassName = 'nf-original-primary-names'
for (const existing of tdEl.querySelectorAll('.' + ulClassName)) {
tdEl.removeChild(existing)
}
const ulEl = document.createElement('ul')
ulEl.classList.add(ulClassName)
const emptyEl = document.createElement('span')
emptyEl.style.opacity = '50%'
emptyEl.textContent = 'empty'
const oldLiEl = document.createElement('li')
oldLiEl.textContent = 'Old: ' + inputEl.value
if (!inputEl.value) {
oldLiEl.appendChild(emptyEl.cloneNode(true))
}
const newLiEl = document.createElement('li')
newLiEl.textContent = 'New: ' + newValue
if (!newValue) {
newLiEl.appendChild(emptyEl.cloneNode(true))
}
ulEl.appendChild(oldLiEl)
ulEl.appendChild(newLiEl)
tdEl.appendChild(ulEl)
inputEl.value = newValue
if (data.defaultNameLanguage === lang) {
console.log(keyEl(i + 1))
keyEl(i + 1).style.fontWeight = '600 !important'
if (choiceEl(i + 2)) choiceEl(i + 2).style.fontWeight = '600'
}
}
// console.log("defaultNameLanguage:", data.defaultNameLanguage)
// console.log("Unspecified:", data.names.filter((el) => "Unspecified" === el.language).map(a => a.value))
const unspecified = data.names.filter((el) => "Unspecified" === el.language).map(a => a.value)
const aliasesLabelEl = document.querySelector('div.editor-field:nth-child(4) > span:nth-child(2)')
if (aliasesLabelEl && !aliasesLabelEl.querySelector('br')) {
aliasesLabelEl.innerHTML += "<br>" + unspecified.join('<br>')
}
// Showing artists
const artistListParentEl = document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)")
const artistListEl = document.createElement("ul")
artistListEl.id = "nf-artists"
for (const existing of artistListParentEl.querySelectorAll("#" + artistListEl.id)) {
artistListParentEl.removeChild(existing)
}
data.artists.forEach(entry => {
const li = document.createElement("li")
const roles = entry.effectiveRoles
const roleInfo = ("Default" === roles ? "D: " + entry.artist?.artistType : roles)
const support = (entry.isSupport ? ", Support" : "")
const artistText = entry.name + " (" + roleInfo + support + ")"
if (entry.isCustomName) {
li.appendChild(document.createTextNode(artistText))
} else {
let link = document.createElement("a")
if (!entry.isCustomName) {
link.href = "/Ar/" + entry.artist.id
}
link.textContent = artistText
li.appendChild(link)
}
artistListEl.appendChild(li)
})
artistListParentEl.appendChild(artistListEl)
})()
// Name Setter variation: Sets the name permanently using React prototypes
// Bookmarklet: javascript:(async()=>{function e(e,t){const n=e instanceof HTMLTextAreaElement?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype;Object.getOwnPropertyDescriptor(n,"value").set.call(e,t);const o=new Event("input",{bubbles:!0});e.dispatchEvent(o)}const t=document.querySelector("a.btn-nomargin:nth-child(1)").href.split("S/")[1],n=`nf:${t}`,o=sessionStorage.getItem(n),l=o?.includes("{")?JSON.parse(o):await fetch(`//${location.host}/api/songs/${t}?fields=Artists,Names&lang=${await(cookieStore.get("UserSettings.LanguagePreference")?.then(e=>e?.value))||"English"}`).then(e=>e.json());sessionStorage.setItem(n,JSON.stringify(l));const i=document.querySelector("div.editor-field:nth-child(2) > select:nth-child(1)");i&&((e,t)=>{const n=Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype,"value")?.set;n?n.call(e,t):e.value=t,e.dispatchEvent(new Event("change",{bubbles:!0})),e.dispatchEvent(new Event("input",{bubbles:!0}))})(i,l.defaultNameLanguage);const a="div.editor-field > table > tbody",c=e=>document.querySelector(a+`> tr:nth-child(${e}) input:nth-child(1)`),s=e=>document.querySelector(a+`> tr:nth-child(${e}) > td`),r=e=>document.querySelector(`div.editor-field:nth-child(2) > select > option:nth-child(${e})`),d=["Japanese","Romaji","English"];for(let n=0;n<d.length;n++){const o=d[n],i=l.names.filter(e=>o===e.language)[0]?.value||"",a=c(n+1),u=a.parentElement;u.style.display="flex",document.querySelector(".ui-tabs")?u.style.gap=".5rem":u.style.flexDirection="column",u.dataset.songId=t;const p="nf-original-primary-names";for(const e of u.querySelectorAll("."+p))u.removeChild(e);const m=document.createElement("ul");m.classList.add(p);const h=document.createElement("span");h.style.opacity="50%",h.textContent="empty";const f=document.createElement("li");f.textContent="Old: "+a.value,a.value||f.appendChild(h.cloneNode(!0));const g=document.createElement("li");g.textContent="New: "+i,i||g.appendChild(h.cloneNode(!0)),m.appendChild(f),m.appendChild(g),u.appendChild(m),e(a,i||""),l.defaultNameLanguage===o&&(console.log(s(n+1)),s(n+1).style.fontWeight="600 !important",r(n+2)&&(r(n+2).style.fontWeight="600"))}const u=l.names.filter(e=>"Unspecified"===e.language).map(e=>e.value),p=document.querySelector("div.editor-field:nth-child(4) > span:nth-child(2)");p&&!p.querySelector("br")&&(p.innerHTML+="<br>"+u.join("<br>"));const m=document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)"),h=document.createElement("ul");h.id="nf-artists";for(const e of m.querySelectorAll("#"+h.id))m.removeChild(e);l.artists.forEach(e=>{const t=document.createElement("li"),n=e.effectiveRoles,o="Default"===n?"D: "+e.artist?.artistType:n,l=e.isSupport?", Support":"",i=e.name+" ("+o+l+")";if(e.isCustomName)t.appendChild(document.createTextNode(i));else{let n=document.createElement("a");e.isCustomName||(n.href="/Ar/"+e.artist.id),n.textContent=i,t.appendChild(n)}h.appendChild(t)}),m.appendChild(h)})();
(async () => {
function setReactInputValue(inputElement, value) {
const prototype = inputElement instanceof HTMLTextAreaElement
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype
const nativeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set
nativeValueSetter.call(inputElement, value)
const event = new Event('input',
{
bubbles: true
}
)
inputElement.dispatchEvent(event)
}
const setReactSelectValue = (selectElement, value) => {
const nativeValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLSelectElement.prototype,
'value'
)?.set
if (nativeValueSetter) {
nativeValueSetter.call(selectElement, value)
} else {
selectElement.value = value
}
selectElement.dispatchEvent(new Event('change', { bubbles: true }))
selectElement.dispatchEvent(new Event('input', { bubbles: true }))
}
// Fetching and caching
const originalId = document.querySelector("a.btn-nomargin:nth-child(1)").href.split("S/")[1]
const cacheKey = `nf:${originalId}`
const cachedRaw = sessionStorage.getItem(cacheKey)
const data = cachedRaw?.includes('{') ? JSON.parse(cachedRaw) : await fetch(`//${location.host}/api/songs/${originalId}?fields=Artists,Names&lang=${await cookieStore.get("UserSettings.LanguagePreference")?.then(c => c?.value) || 'English'}`).then((res) => res.json())
sessionStorage.setItem(cacheKey, JSON.stringify(data))
// Setting and filling names; showing main language and aliases
const defaultNameLanguageSelectEl = document.querySelector('div.editor-field:nth-child(2) > select:nth-child(1)')
if (defaultNameLanguageSelectEl) {
setReactSelectValue(defaultNameLanguageSelectEl, data.defaultNameLanguage)
}
const sameElStr = "div.editor-field > table > tbody"
const valueEl = i => document.querySelector(sameElStr + `> tr:nth-child(${i}) input:nth-child(1)`)
const keyEl = i => document.querySelector(sameElStr + `> tr:nth-child(${i}) > td`)
const choiceEl = i => document.querySelector(`div.editor-field:nth-child(2) > select > option:nth-child(${i})`)
const langs = ['Japanese', 'Romaji', 'English']
for (let i = 0; i < langs.length; i++) {
const lang = langs[i]
const newValue = data.names.filter((el) => lang === el.language)[0]?.value || ''
const inputEl = valueEl(i + 1)
const tdEl = inputEl.parentElement
tdEl.style.display = 'flex'
if (document.querySelector('.ui-tabs')) {
tdEl.style.gap = '.5rem'
} else {
tdEl.style.flexDirection = 'column'
}
tdEl.dataset.songId = originalId
const ulClassName = 'nf-original-primary-names'
for (const existing of tdEl.querySelectorAll('.' + ulClassName)) {
tdEl.removeChild(existing)
}
const ulEl = document.createElement('ul')
ulEl.classList.add(ulClassName)
const emptyEl = document.createElement('span')
emptyEl.style.opacity = '50%'
emptyEl.textContent = 'empty'
const oldLiEl = document.createElement('li')
oldLiEl.textContent = 'Old: ' + inputEl.value
if (!inputEl.value) {
oldLiEl.appendChild(emptyEl.cloneNode(true))
}
const newLiEl = document.createElement('li')
newLiEl.textContent = 'New: ' + newValue
if (!newValue) {
newLiEl.appendChild(emptyEl.cloneNode(true))
}
ulEl.appendChild(oldLiEl)
ulEl.appendChild(newLiEl)
tdEl.appendChild(ulEl)
setReactInputValue(inputEl, newValue || '')
if (data.defaultNameLanguage === lang) {
console.log(keyEl(i + 1))
keyEl(i + 1).style.fontWeight = '600 !important'
if (choiceEl(i + 2)) choiceEl(i + 2).style.fontWeight = '600'
}
}
const unspecified = data.names.filter((el) => "Unspecified" === el.language).map(a => a.value)
const aliasesLabelEl = document.querySelector('div.editor-field:nth-child(4) > span:nth-child(2)')
if (aliasesLabelEl && !aliasesLabelEl.querySelector('br')) {
aliasesLabelEl.innerHTML += "<br>" + unspecified.join('<br>')
}
// Showing artists
const artistListParentEl = document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)")
const artistListEl = document.createElement("ul")
artistListEl.id = "nf-artists"
for (const existing of artistListParentEl.querySelectorAll("#" + artistListEl.id)) {
artistListParentEl.removeChild(existing)
}
data.artists.forEach(entry => {
const li = document.createElement("li")
const roles = entry.effectiveRoles
const roleInfo = ("Default" === roles ? "D: " + entry.artist?.artistType : roles)
const support = (entry.isSupport ? ", Support" : "")
const artistText = entry.name + " (" + roleInfo + support + ")"
if (entry.isCustomName) {
li.appendChild(document.createTextNode(artistText))
} else {
let link = document.createElement("a")
if (!entry.isCustomName) {
link.href = "/Ar/" + entry.artist.id
}
link.textContent = artistText
li.appendChild(link)
}
artistListEl.appendChild(li)
})
artistListParentEl.appendChild(artistListEl)
})()
// Producer Adder extension: Shows all artists and adds producers (for UtaiteDB covers). Based on what I've implemented on VocaDB Scripts.
// Bookmarklet: javascript:(async()=>{const t=".ui-autocomplete .ui-menu-item",e='[id$="tabpane-artists"] tr, .well-transparent div.editor-field:nth-child(12) tr',i=t=>new Promise(e=>setTimeout(e,t)),n=(t,e)=>{const i=t instanceof HTMLTextAreaElement?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype,n=Object.getOwnPropertyDescriptor(i,"value")?.set;n?n.call(t,e):t.value=e,t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0})),t.dispatchEvent(new KeyboardEvent("keydown",{bubbles:!0,key:e.slice(-1)||""})),t.dispatchEvent(new KeyboardEvent("keyup",{bubbles:!0,key:e.slice(-1)||""}))},o=(t,e=1e4)=>new Promise((i,n)=>{const o=document.querySelector(t);if(o&&(o.offsetWidth>0||o.offsetHeight>0))return i(o);const r=setTimeout(()=>{a.disconnect(),n(new Error(`Timeout waiting for ${t}`))},e),a=new MutationObserver(()=>{const e=document.querySelector(t);e&&(e.offsetWidth>0||e.offsetHeight>0)&&(clearTimeout(r),a.disconnect(),i(e))});a.observe(document.body,{childList:!0,subtree:!0,attributes:!0})}),r=(t,e=1e4)=>new Promise((i,n)=>{const o=()=>{const e=document.querySelector(t);return!e||0===e.offsetWidth&&0===e.offsetHeight&&0===e.getClientRects().length};if(o())return i(!0);const r=setTimeout(()=>{a.disconnect(),n(new Error(`Timeout waiting for ${t} to disappear`))},e),a=new MutationObserver(()=>{o()&&(clearTimeout(r),a.disconnect(),i(!0))});a.observe(document.body,{childList:!0,subtree:!0,attributes:!0})}),a=document.querySelector("a.btn-nomargin:nth-child(1)")?.href?.split("S/")[1];if(!a)return;const s=`nf:${a}`,c=sessionStorage.getItem(s),l=c?.includes("{")?JSON.parse(c):await fetch(`//${location.host}/api/songs/${a}?fields=Artists,Names&lang=${await(cookieStore.get("UserSettings.LanguagePreference")?.then(t=>t?.value))||"English"}`).then(t=>t.json());if(sessionStorage.setItem(s,JSON.stringify(l)),!l?.artists||0===l.artists.length)return;const u=document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)"),d=document.createElement("ul");d.id="nf-artists";for(const t of u.querySelectorAll("#"+d.id))u.removeChild(t);l.artists.forEach(t=>{const e=document.createElement("li"),i=t.effectiveRoles,n="Default"===i?"D: "+t.artist?.artistType:i,o=t.isSupport?", Support":"",r=t.name+" ("+n+o+")";if(t.isCustomName)e.appendChild(document.createTextNode(r));else{let i=document.createElement("a");t.isCustomName||(i.href="/Ar/"+t.artist.id),i.textContent=r,e.appendChild(i)}d.appendChild(e)}),u.appendChild(d);const m=l.artists.filter(t=>{const e=t.categories?.split(",").map(t=>t.trim()).includes("Producer");return e});document.querySelector(".ui-tabs")&&await(async t=>{const e=`[id$="tab-${t}"]`;(await o(e)).click(),await i(200)})("artists");const p=new Set(Array.from(document.querySelectorAll('[id$="tabpane-artists"] a.artistLink, .well-transparent a.artistLink')).map(t=>t.getAttribute("href")?.split("/Ar/")[1]).filter(Boolean));for(let a=0;a<m.length;a++){const s=m[a],c=s.isCustomName||!s.artist,l=s.artist?.id?String(s.artist.id):null;if(l&&p.has(l))continue;const u=c?s.name:`id:${l}`,d=document.querySelectorAll(e).length;for(;;)try{const e=await o('[id$="tabpane-artists"] input.input-xlarge:nth-child(5), .well-transparent input.span8:nth-child(4)');e.focus(),n(e,""),await r(t,2e3).catch(()=>{}),n(e,u),await o(t);const i=document.querySelectorAll(t);let a=i[0];c&&(a=i[i.length-1]),a.click();break}catch{await i(300)}const f=Date.now();for(;document.querySelectorAll(e).length===d&&!(Date.now()-f>8e3);)await i(100);if(l&&p.add(l),!document.querySelector(".ui-tabs"))continue;if(s.isSupport){const t=document.querySelector('[id$="tabpane-artists"] tr:last-of-type input[type="checkbox"]');t&&!t.checked&&t.click()}const h=s.roles;if(h&&"Default"!==h){const t=h.split(",").map(t=>t.trim()).filter(Boolean);if(t.length>0){const e=await o('[id$="tabpane-artists"] tr:last-of-type .artistRolesEdit');e.click(),await o(".ui-dialog");const n=document.querySelector(".ui-dialog button.ui-button:nth-child(1)");let r=[];for(;r=Array.from(document.querySelectorAll(".ui-dialog span.tag > label")),0===r.length;)n?(n.click(),await i(900),e.click()):await i(1e3);for(const e of r)t.includes(e.textContent.trim())&&e.click();n&&n.click(),await i(200)}}await i(100)}})();
(async () => {
const autocompleteSelector = '.ui-autocomplete .ui-menu-item'
const newArtistSelector = '[id$="tabpane-artists"] input.input-xlarge:nth-child(5), .well-transparent input.span8:nth-child(4)'
const supportSelector = '[id$="tabpane-artists"] tr:last-of-type input[type="checkbox"]'
const roleCustomizeSelector = '[id$="tabpane-artists"] tr:last-of-type .artistRolesEdit'
const rolesSaveSelector = '.ui-dialog button.ui-button:nth-child(1)'
const roleButtonSelector = '.ui-dialog span.tag > label'
const artistLinkSelector = '[id$="tabpane-artists"] a.artistLink, .well-transparent a.artistLink'
const artistRowsSelector = '[id$="tabpane-artists"] tr, .well-transparent div.editor-field:nth-child(12) tr'
const originalIdLinkSelector = 'a.btn-nomargin:nth-child(1)'
const dialogSelector = '.ui-dialog'
const sleep = ms => new Promise(r => setTimeout(r, ms))
const setReactInputValue = (inputElement, value) => {
const prototype = inputElement instanceof HTMLTextAreaElement
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype
const nativeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set
if (nativeValueSetter) {
nativeValueSetter.call(inputElement, value)
} else {
inputElement.value = value
}
inputElement.dispatchEvent(new Event('input', { bubbles: true }))
inputElement.dispatchEvent(new Event('change', { bubbles: true }))
inputElement.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: value.slice(-1) || '' }))
inputElement.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: value.slice(-1) || '' }))
}
const waitFor = (selector, timeout = 10000) => new Promise((resolve, reject) => {
const el = document.querySelector(selector)
if (el && (el.offsetWidth > 0 || el.offsetHeight > 0)) {
return resolve(el)
}
const timer = setTimeout(() => {
observer.disconnect()
reject(new Error(`Timeout waiting for ${selector}`))
}, timeout)
const observer = new MutationObserver(() => {
const target = document.querySelector(selector)
if (target && (target.offsetWidth > 0 || target.offsetHeight > 0)) {
clearTimeout(timer)
observer.disconnect()
resolve(target)
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
})
})
const waitForNone = (selector, timeout = 10000) => new Promise((resolve, reject) => {
const isGoneOrHidden = () => {
const el = document.querySelector(selector)
return !el || (el.offsetWidth === 0 && el.offsetHeight === 0 && el.getClientRects().length === 0)
}
if (isGoneOrHidden()) {
return resolve(true)
}
const timer = setTimeout(() => {
observer.disconnect()
reject(new Error(`Timeout waiting for ${selector} to disappear`))
}, timeout)
const observer = new MutationObserver(() => {
if (isGoneOrHidden()) {
clearTimeout(timer)
observer.disconnect()
resolve(true)
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
})
})
const switchToTab = async (tabId) => {
const tabSelector = `[id$="tab-${tabId}"]`
const tabElem = await waitFor(tabSelector)
tabElem.click()
await sleep(200)
}
const getExistingArtistIds = () => {
return new Set(
Array.from(document.querySelectorAll(artistLinkSelector)).map((a) => a.getAttribute('href')?.split('/Ar/')[1]).filter(Boolean)
)
}
// Fetching and caching
const originalId = document.querySelector(originalIdLinkSelector)?.href?.split('S/')[1]
if (!originalId) return
const cacheKey = `nf:${originalId}`
const cachedRaw = sessionStorage.getItem(cacheKey)
const data = cachedRaw?.includes('{') ? JSON.parse(cachedRaw) : await fetch(`//${location.host}/api/songs/${originalId}?fields=Artists,Names&lang=${await cookieStore.get("UserSettings.LanguagePreference")?.then(c => c?.value) || 'English'}`).then((res) => res.json())
sessionStorage.setItem(cacheKey, JSON.stringify(data))
if (!data?.artists || data.artists.length === 0) return
// Showing artists
const artistListParentEl = document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)")
const artistListEl = document.createElement("ul")
artistListEl.id = "nf-artists"
for (const existing of artistListParentEl.querySelectorAll("#" + artistListEl.id)) {
artistListParentEl.removeChild(existing)
}
data.artists.forEach(entry => {
const li = document.createElement("li")
const roles = entry.effectiveRoles
const roleInfo = ("Default" === roles ? "D: " + entry.artist?.artistType : roles)
const support = (entry.isSupport ? ", Support" : "")
const artistText = entry.name + " (" + roleInfo + support + ")"
if (entry.isCustomName) {
li.appendChild(document.createTextNode(artistText))
} else {
let link = document.createElement("a")
if (!entry.isCustomName) {
link.href = "/Ar/" + entry.artist.id
}
link.textContent = artistText
li.appendChild(link)
}
artistListEl.appendChild(li)
})
artistListParentEl.appendChild(artistListEl)
// Adding artists
const eligibleArtists = data.artists.filter((entry) => {
const hasProducerRole = entry.categories?.split(',').map((role) => role.trim()).includes('Producer')
return hasProducerRole
})
if (document.querySelector('.ui-tabs')) await switchToTab('artists')
const existingIds = getExistingArtistIds()
for (let i = 0; i < eligibleArtists.length; i++) {
const entry = eligibleArtists[i]
const isCustom = entry.isCustomName || !entry.artist
const artistId = entry.artist?.id ? String(entry.artist.id) : null
if (artistId && existingIds.has(artistId)) continue
const artistInput = isCustom ? entry.name : `id:${artistId}`
const prevRowCount = document.querySelectorAll(artistRowsSelector).length
while (true) {
try {
const newArtistEl = await waitFor(newArtistSelector)
newArtistEl.focus()
setReactInputValue(newArtistEl, '')
await waitForNone(autocompleteSelector, 2000).catch(() => { })
setReactInputValue(newArtistEl, artistInput)
await waitFor(autocompleteSelector)
const autocompleteEls = document.querySelectorAll(autocompleteSelector)
let selectedEl = autocompleteEls[0]
if (isCustom) {
selectedEl = autocompleteEls[autocompleteEls.length - 1]
}
selectedEl.click()
break
} catch {
await sleep(300)
}
}
const startWait = Date.now()
while (document.querySelectorAll(artistRowsSelector).length === prevRowCount) {
if (Date.now() - startWait > 8000) break
await sleep(100)
}
if (artistId) existingIds.add(artistId)
if (!document.querySelector('.ui-tabs')) continue
if (entry.isSupport) {
const supportEl = document.querySelector(supportSelector)
if (supportEl && !supportEl.checked) {
supportEl.click()
}
}
const rolesStr = entry.roles
if (rolesStr && rolesStr !== 'Default') {
const targetRoles = rolesStr.split(',').map((r) => r.trim()).filter(Boolean)
if (targetRoles.length > 0) {
const roleCustomizeEl = await waitFor(roleCustomizeSelector)
roleCustomizeEl.click()
await waitFor(dialogSelector)
const rolesSaveEl = document.querySelector(rolesSaveSelector)
let roleButtonEls = []
while (true) {
roleButtonEls = Array.from(document.querySelectorAll(roleButtonSelector))
if (roleButtonEls.length === 0) {
if (rolesSaveEl) {
rolesSaveEl.click()
await sleep(900)
roleCustomizeEl.click()
continue
}
await sleep(1000)
} else {
break
}
}
for (const btn of roleButtonEls) {
if (targetRoles.includes(btn.textContent.trim())) {
btn.click()
}
}
if (rolesSaveEl) {
rolesSaveEl.click()
}
await sleep(200)
}
}
await sleep(100)
}
})()
// Vocalist Adder extension: Shows all artists and adds vocalists (for derivatives on VocaDB). Based on what I've implemented on VocaDB Scripts.
// Bookmarklet: javascript:(async()=>{const t=".ui-autocomplete .ui-menu-item",e='[id$="tabpane-artists"] tr, .well-transparent div.editor-field:nth-child(12) tr',i=t=>new Promise(e=>setTimeout(e,t)),n=(t,e)=>{const i=t instanceof HTMLTextAreaElement?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype,n=Object.getOwnPropertyDescriptor(i,"value")?.set;n?n.call(t,e):t.value=e,t.dispatchEvent(new Event("input",{bubbles:!0})),t.dispatchEvent(new Event("change",{bubbles:!0})),t.dispatchEvent(new KeyboardEvent("keydown",{bubbles:!0,key:e.slice(-1)||""})),t.dispatchEvent(new KeyboardEvent("keyup",{bubbles:!0,key:e.slice(-1)||""}))},o=(t,e=1e4)=>new Promise((i,n)=>{const o=document.querySelector(t);if(o&&(o.offsetWidth>0||o.offsetHeight>0))return i(o);const a=setTimeout(()=>{r.disconnect(),n(new Error(`Timeout waiting for ${t}`))},e),r=new MutationObserver(()=>{const e=document.querySelector(t);e&&(e.offsetWidth>0||e.offsetHeight>0)&&(clearTimeout(a),r.disconnect(),i(e))});r.observe(document.body,{childList:!0,subtree:!0,attributes:!0})}),a=(t,e=1e4)=>new Promise((i,n)=>{const o=()=>{const e=document.querySelector(t);return!e||0===e.offsetWidth&&0===e.offsetHeight&&0===e.getClientRects().length};if(o())return i(!0);const a=setTimeout(()=>{r.disconnect(),n(new Error(`Timeout waiting for ${t} to disappear`))},e),r=new MutationObserver(()=>{o()&&(clearTimeout(a),r.disconnect(),i(!0))});r.observe(document.body,{childList:!0,subtree:!0,attributes:!0})}),r=document.querySelector("a.btn-nomargin:nth-child(1)")?.href?.split("S/")[1];if(!r)return;const s=`nf:${r}`,c=sessionStorage.getItem(s),l=c?.includes("{")?JSON.parse(c):await fetch(`//${location.host}/api/songs/${r}?fields=Artists,Names&lang=${await(cookieStore.get("UserSettings.LanguagePreference")?.then(t=>t?.value))||"English"}`).then(t=>t.json());if(sessionStorage.setItem(s,JSON.stringify(l)),!l?.artists||0===l.artists.length)return;const u=document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)"),d=document.createElement("ul");d.id="nf-artists";for(const t of u.querySelectorAll("#"+d.id))u.removeChild(t);l.artists.forEach(t=>{const e=document.createElement("li"),i=t.effectiveRoles,n="Default"===i?"D: "+t.artist?.artistType:i,o=t.isSupport?", Support":"",a=t.name+" ("+n+o+")";if(t.isCustomName)e.appendChild(document.createTextNode(a));else{let i=document.createElement("a");t.isCustomName||(i.href="/Ar/"+t.artist.id),i.textContent=a,e.appendChild(i)}d.appendChild(e)}),u.appendChild(d);const m=l.artists.filter(t=>{const e=t.categories?.split(",").map(t=>t.trim()).includes("Vocalist");return e});document.querySelector(".ui-tabs")&&await(async t=>{const e=`[id$="tab-${t}"]`;(await o(e)).click(),await i(200)})("artists");const p=new Set(Array.from(document.querySelectorAll('[id$="tabpane-artists"] a.artistLink, .well-transparent a.artistLink')).map(t=>t.getAttribute("href")?.split("/Ar/")[1]).filter(Boolean));for(let r=0;r<m.length;r++){const s=m[r],c=s.isCustomName||!s.artist,l=s.artist?.id?String(s.artist.id):null;if(l&&p.has(l))continue;const u=c?s.name:`id:${l}`,d=document.querySelectorAll(e).length;for(;;)try{const e=await o('[id$="tabpane-artists"] input.input-xlarge:nth-child(5), .well-transparent input.span8:nth-child(4)');e.focus(),n(e,""),await a(t,2e3).catch(()=>{}),n(e,u),await o(t);const i=document.querySelectorAll(t);let r=i[0];c&&(r=i[i.length-1]),r.click();break}catch{await i(300)}const f=Date.now();for(;document.querySelectorAll(e).length===d&&!(Date.now()-f>8e3);)await i(100);if(l&&p.add(l),!document.querySelector(".ui-tabs"))continue;if(s.isSupport){const t=document.querySelector('[id$="tabpane-artists"] tr:last-of-type input[type="checkbox"]');t&&!t.checked&&t.click()}const h=s.roles;if(h&&"Default"!==h){const t=h.split(",").map(t=>t.trim()).filter(Boolean);if(t.length>0){const e=await o('[id$="tabpane-artists"] tr:last-of-type .artistRolesEdit');e.click(),await o(".ui-dialog");const n=document.querySelector(".ui-dialog button.ui-button:nth-child(1)");let a=[];for(;a=Array.from(document.querySelectorAll(".ui-dialog span.tag > label")),0===a.length;)n?(n.click(),await i(900),e.click()):await i(1e3);for(const e of a)t.includes(e.textContent.trim())&&e.click();n&&n.click(),await i(200)}}await i(100)}})();
(async () => {
const autocompleteSelector = '.ui-autocomplete .ui-menu-item'
const newArtistSelector = '[id$="tabpane-artists"] input.input-xlarge:nth-child(5), .well-transparent input.span8:nth-child(4)'
const supportSelector = '[id$="tabpane-artists"] tr:last-of-type input[type="checkbox"]'
const roleCustomizeSelector = '[id$="tabpane-artists"] tr:last-of-type .artistRolesEdit'
const rolesSaveSelector = '.ui-dialog button.ui-button:nth-child(1)'
const roleButtonSelector = '.ui-dialog span.tag > label'
const artistLinkSelector = '[id$="tabpane-artists"] a.artistLink, .well-transparent a.artistLink'
const artistRowsSelector = '[id$="tabpane-artists"] tr, .well-transparent div.editor-field:nth-child(12) tr'
const originalIdLinkSelector = 'a.btn-nomargin:nth-child(1)'
const dialogSelector = '.ui-dialog'
const sleep = ms => new Promise(r => setTimeout(r, ms))
const setReactInputValue = (inputElement, value) => {
const prototype = inputElement instanceof HTMLTextAreaElement
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype
const nativeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set
if (nativeValueSetter) {
nativeValueSetter.call(inputElement, value)
} else {
inputElement.value = value
}
inputElement.dispatchEvent(new Event('input', { bubbles: true }))
inputElement.dispatchEvent(new Event('change', { bubbles: true }))
inputElement.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: value.slice(-1) || '' }))
inputElement.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: value.slice(-1) || '' }))
}
const waitFor = (selector, timeout = 10000) => new Promise((resolve, reject) => {
const el = document.querySelector(selector)
if (el && (el.offsetWidth > 0 || el.offsetHeight > 0)) {
return resolve(el)
}
const timer = setTimeout(() => {
observer.disconnect()
reject(new Error(`Timeout waiting for ${selector}`))
}, timeout)
const observer = new MutationObserver(() => {
const target = document.querySelector(selector)
if (target && (target.offsetWidth > 0 || target.offsetHeight > 0)) {
clearTimeout(timer)
observer.disconnect()
resolve(target)
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
})
})
const waitForNone = (selector, timeout = 10000) => new Promise((resolve, reject) => {
const isGoneOrHidden = () => {
const el = document.querySelector(selector)
return !el || (el.offsetWidth === 0 && el.offsetHeight === 0 && el.getClientRects().length === 0)
}
if (isGoneOrHidden()) {
return resolve(true)
}
const timer = setTimeout(() => {
observer.disconnect()
reject(new Error(`Timeout waiting for ${selector} to disappear`))
}, timeout)
const observer = new MutationObserver(() => {
if (isGoneOrHidden()) {
clearTimeout(timer)
observer.disconnect()
resolve(true)
}
})
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
})
})
const switchToTab = async (tabId) => {
const tabSelector = `[id$="tab-${tabId}"]`
const tabElem = await waitFor(tabSelector)
tabElem.click()
await sleep(200)
}
const getExistingArtistIds = () => {
return new Set(
Array.from(document.querySelectorAll(artistLinkSelector)).map((a) => a.getAttribute('href')?.split('/Ar/')[1]).filter(Boolean)
)
}
// Fetching and caching
const originalId = document.querySelector(originalIdLinkSelector)?.href?.split('S/')[1]
if (!originalId) return
const cacheKey = `nf:${originalId}`
const cachedRaw = sessionStorage.getItem(cacheKey)
const data = cachedRaw?.includes('{') ? JSON.parse(cachedRaw) : await fetch(`//${location.host}/api/songs/${originalId}?fields=Artists,Names&lang=${await cookieStore.get("UserSettings.LanguagePreference")?.then(c => c?.value) || 'English'}`).then((res) => res.json())
sessionStorage.setItem(cacheKey, JSON.stringify(data))
if (!data?.artists || data.artists.length === 0) return
// Showing artists
const artistListParentEl = document.querySelector(".editor-label .extraInfo, [id$=tabpane-artists] .span4:nth-child(2)")
const artistListEl = document.createElement("ul")
artistListEl.id = "nf-artists"
for (const existing of artistListParentEl.querySelectorAll("#" + artistListEl.id)) {
artistListParentEl.removeChild(existing)
}
data.artists.forEach(entry => {
const li = document.createElement("li")
const roles = entry.effectiveRoles
const roleInfo = ("Default" === roles ? "D: " + entry.artist?.artistType : roles)
const support = (entry.isSupport ? ", Support" : "")
const artistText = entry.name + " (" + roleInfo + support + ")"
if (entry.isCustomName) {
li.appendChild(document.createTextNode(artistText))
} else {
let link = document.createElement("a")
if (!entry.isCustomName) {
link.href = "/Ar/" + entry.artist.id
}
link.textContent = artistText
li.appendChild(link)
}
artistListEl.appendChild(li)
})
artistListParentEl.appendChild(artistListEl)
// Adding artists
const eligibleArtists = data.artists.filter((entry) => {
const hasVocalistRole = entry.categories?.split(',').map((role) => role.trim()).includes('Vocalist')
return hasVocalistRole
})
if (document.querySelector('.ui-tabs')) await switchToTab('artists')
const existingIds = getExistingArtistIds()
for (let i = 0; i < eligibleArtists.length; i++) {
const entry = eligibleArtists[i]
const isCustom = entry.isCustomName || !entry.artist
const artistId = entry.artist?.id ? String(entry.artist.id) : null
if (artistId && existingIds.has(artistId)) continue
const artistInput = isCustom ? entry.name : `id:${artistId}`
const prevRowCount = document.querySelectorAll(artistRowsSelector).length
while (true) {
try {
const newArtistEl = await waitFor(newArtistSelector)
newArtistEl.focus()
setReactInputValue(newArtistEl, '')
await waitForNone(autocompleteSelector, 2000).catch(() => { })
setReactInputValue(newArtistEl, artistInput)
await waitFor(autocompleteSelector)
const autocompleteEls = document.querySelectorAll(autocompleteSelector)
let selectedEl = autocompleteEls[0]
if (isCustom) {
selectedEl = autocompleteEls[autocompleteEls.length - 1]
}
selectedEl.click()
break
} catch {
await sleep(300)
}
}
const startWait = Date.now()
while (document.querySelectorAll(artistRowsSelector).length === prevRowCount) {
if (Date.now() - startWait > 8000) break
await sleep(100)
}
if (artistId) existingIds.add(artistId)
if (!document.querySelector('.ui-tabs')) continue
if (entry.isSupport) {
const supportEl = document.querySelector(supportSelector)
if (supportEl && !supportEl.checked) {
supportEl.click()
}
}
const rolesStr = entry.roles
if (rolesStr && rolesStr !== 'Default') {
const targetRoles = rolesStr.split(',').map((r) => r.trim()).filter(Boolean)
if (targetRoles.length > 0) {
const roleCustomizeEl = await waitFor(roleCustomizeSelector)
roleCustomizeEl.click()
await waitFor(dialogSelector)
const rolesSaveEl = document.querySelector(rolesSaveSelector)
let roleButtonEls = []
while (true) {
roleButtonEls = Array.from(document.querySelectorAll(roleButtonSelector))
if (roleButtonEls.length === 0) {
if (rolesSaveEl) {
rolesSaveEl.click()
await sleep(900)
roleCustomizeEl.click()
continue
}
await sleep(1000)
} else {
break
}
}
for (const btn of roleButtonEls) {
if (targetRoles.includes(btn.textContent.trim())) {
btn.click()
}
}
if (rolesSaveEl) {
rolesSaveEl.click()
}
await sleep(200)
}
}
await sleep(100)
}
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment