Last active
March 2, 2024 06:20
-
-
Save ToadKing/fe6ed8c1117b8a76018847dbc72865d5 to your computer and use it in GitHub Desktop.
This file contains 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<title>Relationship Combiner</title> | |
<style> | |
#output { | |
white-space: pre-wrap; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Relationship Combiner</h1> | |
<select id="parseType"> | |
<option value="perTrack">Per Track</option> | |
<option value="perDisc">Per Disc</option> | |
</select> | |
<br> | |
<textarea id="input" rows="24" cols="80"></textarea> | |
<div id="output"></div> | |
<script> | |
function parse() { | |
try { | |
const input = String(document.getElementById('input').value.replaceAll('\r\n', '\n').trim()) | |
const parsed = {} | |
let maxMediumLength = 1 | |
let maxTrackLength = 1 | |
switch (document.getElementById('parseType').value) { | |
case 'perTrack': { | |
let curMedium = 0 | |
let curTrack = 0 | |
for (const line of input.split('\n')) { | |
let newTrack = line.match(/M[-.](?:(\d+)[-.])?(\d+)/i) | |
if (newTrack) { | |
curMedium = newTrack[1] ? Number(newTrack[1]) : 1 | |
curTrack = Number(newTrack[2]) | |
if (curMedium === 0 || curTrack === 0) { | |
throw new RangeError('medium or track should never be set to 0') | |
} | |
if (String(curMedium).length > maxMediumLength) { | |
maxMediumLength = String(curMedium).length | |
} | |
if (String(curTrack).length > maxTrackLength) { | |
maxTrackLength = String(curTrack).length | |
} | |
continue | |
} | |
if (!line.includes(':')) { | |
continue | |
} | |
if (curMedium === 0 || curTrack === 0) { | |
throw new RangeError('medium or track not set before first roles') | |
} | |
const [rawRoles, rawArtists] = line.split(':') | |
const roles = rawRoles.split(',').map((r) => r.trim()) | |
const artists = rawArtists.split(',').map((r) => r.trim()) | |
for (const role of roles) { | |
if (!parsed[role]) { | |
parsed[role] = {} | |
} | |
for (const artist of artists) { | |
if (!parsed[role][artist]) { | |
parsed[role][artist] = new Set() | |
} | |
parsed[role][artist].add([curMedium, curTrack]) | |
} | |
} | |
} | |
break | |
} | |
case 'perDisc': { | |
let curMedium | |
let curRole | |
for (const rawline of input.split('\n')) { | |
const line = rawline.trim() | |
if (!line) { | |
continue | |
} | |
const newDisc = line.match(/^Disc (\d+)$/i) | |
if (newDisc) { | |
curMedium = Number(newDisc[1]) | |
curRole = undefined | |
if (String(curMedium).length > maxMediumLength) { | |
maxMediumLength = String(curMedium).length | |
} | |
continue | |
} | |
const newRole = line.match(/^(.*?)(?: by:|:| by)$/i) | |
if (newRole) { | |
curRole = newRole[1].trim() | |
continue | |
} | |
const artistsAndTracks = line.match(/^(.*) \(([\d,-~ ]+)\)$/) | |
if (artistsAndTracks) { | |
const [, rawartists, rawtracks] = artistsAndTracks | |
const artists = rawartists.split(',').map(a => a.trim()) | |
const tracks = rawtracks.split(',').map(t => { | |
const range = t.trim().match(/^(\d+)[-~](\d+)$/) | |
if (range) { | |
const [, rawfirst, rawlast] = range | |
const first = Number(rawfirst) | |
const last = Number(rawlast) | |
if (!(last > first)) { | |
throw new RangeError('range not start~end') | |
} | |
const nums = [] | |
for (let i = first; i <= last; i++) { | |
nums.push(i) | |
} | |
if (String(last).length > maxTrackLength) { | |
maxTrackLength = String(last).length | |
} | |
return nums | |
} else { | |
const num = Number(t) | |
if (!num) { | |
throw new RangeError(`bad track number: ${t}`) | |
} | |
if (String(num).length > maxTrackLength) { | |
maxTrackLength = String(num).length | |
} | |
return [num] | |
} | |
}).flat().map(t => [curMedium, t]) | |
if (!parsed[curRole]) { | |
parsed[curRole] = {} | |
} | |
for (const artist of artists) { | |
if (!parsed[curRole][artist]) { | |
parsed[curRole][artist] = new Set() | |
} | |
for (const track of tracks) { | |
parsed[curRole][artist].add(track) | |
} | |
} | |
continue | |
} | |
throw new RangeError(`could not parse line: ${line}`) | |
} | |
break | |
} | |
} | |
let out = '' | |
for (const [role, artistsAndTracks] of Object.entries(parsed)) { | |
out += `${role}:\n` | |
for (const [artist, unsortedTracks] of Object.entries(artistsAndTracks)) { | |
out += `${artist} (` | |
const tracks = [...unsortedTracks].sort(([am, at], [bm, bt]) => am - bm === 0 ? at - bt : am - bm) | |
const formattedTracks = [] | |
for (let i = 0; i < tracks.length; i++) { | |
const [medium, track] = tracks[i] | |
// format three or more tracks sequentially like 'D.M~N' | |
if (i + 2 < tracks.length && tracks[i + 1][0] === medium && tracks[i + 1][1] === track + 1 && tracks[i + 2][0] === medium && tracks[i + 2][1] === track + 2) { | |
let endRange = track + 1 | |
while (i + 1 < tracks.length && tracks[i + 1][0] === medium && tracks[i + 1][1] === endRange) { | |
i += 1 | |
endRange = endRange + 1 | |
} | |
formattedTracks.push(`${String(medium).padStart(maxMediumLength, '0')}.${String(track).padStart(maxTrackLength, '0')}~${String(endRange - 1).padStart(maxTrackLength, '0')}`) | |
} else { | |
formattedTracks.push(`${String(medium).padStart(maxMediumLength, '0')}.${String(track).padStart(maxTrackLength, '0')}`) | |
} | |
} | |
out += formattedTracks.join(', ') | |
out += ')\n' | |
} | |
out += '\n' | |
} | |
document.getElementById('output').textContent = out | |
} catch (e) { | |
document.getElementById('output').textContent = e | |
} | |
} | |
document.getElementById('input').addEventListener('input', parse) | |
document.getElementById('parseType').addEventListener('change', parse) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment