Last active
August 17, 2026 14:41
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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) | |
| })() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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) | |
| })() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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) | |
| } | |
| })() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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