Skip to content

Instantly share code, notes, and snippets.

@ggorlen
Created June 16, 2025 03:56
Show Gist options
  • Save ggorlen/c294812b52ee1c5cb87e2d1728499ed3 to your computer and use it in GitHub Desktop.
Save ggorlen/c294812b52ee1c5cb87e2d1728499ed3 to your computer and use it in GitHub Desktop.
discogs video adder
// WIP script that auto-adds videos to a discogs release
// TODO: "full album" mode
// TODO: try multiple approaches (with and without artist/album title)
/*
x=[...document.querySelectorAll("[data-track-position]")]
.map(e => e.querySelector("td:nth-child(3) span").textContent).join("\n")
console.log(x)
*/
const titles = `
Imperium In Imperio
Divine Dictation
The Opportunist
Relentless Hatred
Sacrificial Hire
War For Resources
Black Banner
Hara-Kiri
Stale Affairs
Regressive Agenda
Incarceration State
Unit 731
Icaro
Husayni / Handschar
Pharmacide
`.trim().split("\n");
(async () => {
function setNativeValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, "value")?.set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
}
function findInputByLabelText(labelText) {
return [...document.querySelectorAll('label')].reduce((found, label) => {
if (found) return found;
if (label.textContent.trim() === labelText.trim()) {
return label.htmlFor
? document.getElementById(label.htmlFor)
: label.querySelector('input, textarea, select');
}
return null;
}, null);
}
function findButtonByText(text) {
return [...document.querySelectorAll("button")].find(btn => btn.textContent.trim() === text);
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function levenshtein(a, b) {
const matrix = Array.from({ length: b.length + 1 }, (_, i) =>
Array(a.length + 1).fill(0)
);
for (let i = 0; i <= b.length; i++) matrix[i][0] = i;
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
matrix[i][j] = b[i - 1] === a[j - 1]
? matrix[i - 1][j - 1]
: Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + 1
);
}
}
return matrix[b.length][a.length];
}
const albumTitle = document.querySelector("[class*=title]").textContent;
for (const title of titles) {
const input = findInputByLabelText("YouTube search query:");
setNativeValue(input, `${albumTitle} ${title} "provided"`);
input.dispatchEvent(new Event("input", { bubbles: true }));
findButtonByText("Search")?.click();
await delay(1000);
//const videos = await new Promise(resolve => {
// (function update() {
// const videos =
// [...document.querySelectorAll("[class*='results'] li")];
//
// if (videos.length) {
// return resolve(videos);
// }
//
// requestAnimationFrame(update);
// })();
//});
const videos = [...document.querySelectorAll("[class*='results'] li")];
let bestMatch = null;
let minDistance = Infinity;
for (const video of videos) {
const link = video.querySelector("a");
const resultTitle = link.textContent.trim();
const dist = levenshtein(title.toLowerCase(), resultTitle.toLowerCase());
if (dist < minDistance) {
minDistance = dist;
bestMatch = video;
}
}
if (bestMatch) {
findButtonByText("Add")?.click();
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment