Skip to content

Instantly share code, notes, and snippets.

@ToadKing
Last active April 2, 2024 04:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ToadKing/923288ca6e62cf9130828638ab6e0423 to your computer and use it in GitHub Desktop.
Save ToadKing/923288ca6e62cf9130828638ab6e0423 to your computer and use it in GitHub Desktop.
Musicbrainz - Combine Catalog Numbers
// ==UserScript==
// @name Musicbrainz - Combine Catalog Numbers
// @namespace com.toadking.mb.catalog
// @match https://musicbrainz.org/*
// @grant none
// @version 1.4
// @author Toad King
// ==/UserScript==
const catNumRegExp = /^(.*?)(\d+)([^\d]*)$/
function parseCatNums(catalogNumbers) {
let splitCatNums = []
for (const catNum of catalogNumbers) {
const catnumSearch = catNum.match(catNumRegExp)
if (!catnumSearch) {
splitCatNums.push({ prefix: catNum, suffix: '' })
} else {
const [, prefix, num, suffix] = catnumSearch
splitCatNums.push({ prefix, num, suffix })
}
}
let parsedCatNums = []
while (splitCatNums.length > 0) {
const { prefix, suffix, num } = splitCatNums[0]
// skip non-matching catalog numbers or number-only ones
if ((prefix == '' && suffix === '') || num === undefined) {
parsedCatNums.push(prefix + (num !== undefined ? num : '') + suffix)
splitCatNums.shift()
} else {
const matchedPrefixes = splitCatNums.filter(c => c.num !== undefined && c.prefix === prefix && c.suffix === suffix).sort((a, b) => Number(a.num) - Number(b.num))
splitCatNums = splitCatNums.filter(c => c.num === undefined || c.prefix !== prefix)
const padSize = matchedPrefixes[0].num.length
const ranges = []
let currentRange = { start: Number(matchedPrefixes[0].num), end: Number(matchedPrefixes[0].num) }
for (const catNum of matchedPrefixes.slice(1)) {
const num = Number(catNum.num)
if (num === currentRange.end + 1) {
currentRange.end = num
} else {
ranges.push(currentRange)
currentRange = { start: num, end: num }
}
}
ranges.push(currentRange)
for (const { start, end } of ranges) {
let endString = String(end).padStart(padSize, '0')
if (start === end) {
parsedCatNums.push(prefix + endString)
} else {
// use endString.length to handle cases like 99~100
let startString = String(start).padStart(endString.length, '0')
while (startString.length > 0) {
if (startString[0] !== endString[0]) {
break
}
startString = startString.substring(1)
endString = endString.substring(1)
}
parsedCatNums.push(prefix + String(start).padStart(padSize, '0') + '~' + endString + suffix)
}
}
}
}
return parsedCatNums
}
const catalogLists = new Set(Array.from(document.querySelectorAll('.catalog-number')).map(e => e.parentElement))
for (const listElem of catalogLists) {
const catalogNumbers = new Set(Array.from(listElem.querySelectorAll('.catalog-number')).map(e => e.textContent))
// skip elements that contain only one catalog number (also includes label lists on release pages)
if (catalogNumbers.size <= 1) {
continue
}
const parsedCatNums = parseCatNums(catalogNumbers)
const newElems = parsedCatNums.map(c => {
const span = document.createElement('span')
span.className = 'catalog-number'
span.textContent = c
return span
})
listElem.replaceChildren(newElems.shift())
for (const elem of newElems) {
listElem.appendChild(document.createTextNode(', '))
listElem.appendChild(elem)
}
}
// Tests
// const tests = [
// // Long list
// {
// input: 'SBPS-0076, SBPS-0077, SBPS-0078, SBPS-0079, SBPS-0080, SBPS-0081, SBPS-0082, SBPS-0083, SBPS-0084, SBPS-0085, SBPS-0086, SBPS-0087, SBPS-0088, SBPS-0089, SBPS-0090, SBPS-0091, SBPS-0092, SBPS-0093, SBPS-0094, SBPS-0095'.split(', '),
// expected: ['SBPS-0076~95'],
// },
// // Reverse order
// {
// input: 'VPCG-84243, VPCG-84242, VPCG-84241, VPCG-84240, VPCG-84239, VPCG-84238, VPCG-84237, VPCG-84236, VPCG-84235, VPCG-84234'.split(', '),
// expected: ['VPCG-84234~43'],
// },
// // No dashes
// {
// input: 'LC2493, LC2494, LC2495, LC2496, LC2497, LC2498, LC2499, LC2500, LC2501, LC2502, LC2503, LC2504, LC2505'.split(', '),
// expected: ['LC2493~505'],
// },
// // Dashes
// {
// input: 'GFCA-525, GFCA-526, GFCA-527, GFCA-528, GFCA-529, GFCA-530, GFCA-531, GFCA-532, GFCA-533, GFCA-534, GFCA-535, GFCA-536, GFCA-537'.split(', '),
// expected: ['GFCA-525~37'],
// },
// // Split in ranges
// {
// input: 'TEST-01, TEST-02, TEST-04, TEST-05'.split(', '),
// expected: ['TEST-01~2', 'TEST-04~5'],
// },
// // Padding increases
// {
// input: 'VTJL-10, VTJL-9'.split(', '),
// expected: ['VTJL-9~10'],
// },
// // Numbers only
// {
// input: '1, 2, 3'.split(', '),
// expected: ['1', '2', '3'],
// },
// // no catalog numbers
// {
// input: [],
// expected: [],
// },
// // no numbers
// {
// input: 'foo, bar'.split(', '),
// expected: ['foo', 'bar'],
// },
// // Prefix and Suffix
// {
// input: 'KBCI-00008A, KBCI-00009A, KBCI-00010A, KBCI-00011A, KBCI-00012A, KBCI-00013A'.split(', '),
// expected: ['KBCI-00008~13A'],
// },
// // Suffix only
// {
// input: '00008A, 00009A, 00010A'.split(', '),
// expected: ['00008~10A'],
// },
// // Two number areas
// {
// input: '1-00008, 1-00009, 1-00010'.split(', '),
// expected: ['1-00008~10'],
// },
// // Two number areas with suffix
// {
// input: '1-00008A, 1-00009A, 1-00010A'.split(', '),
// expected: ['1-00008~10A'],
// },
// ]
// for (const { input, expected } of tests) {
// const output = parseCatNums(input)
// if (JSON.stringify(output) !== JSON.stringify(expected)) {
// throw new Error(`test failed (expected ${expected}, got ${output})`)
// }
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment