Last active
December 28, 2024 03:57
-
-
Save ToadKing/fe6ed8c1117b8a76018847dbc72865d5 to your computer and use it in GitHub Desktop.
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
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>Relationship Combiner</title> | |
<style> | |
#output { | |
white-space: pre-wrap; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>Relationship Combiner</h1> | |
<label>Input format: | |
<select id="parseType"> | |
<option value="perTrack">Per Track</option> | |
<option value="perDisc">Per Disc</option> | |
</select> | |
</label> | |
<br> | |
<textarea id="input" rows="24" cols="80"></textarea> | |
<br> | |
<label>Output format: | |
<select id="outputType"> | |
<option value="ranges">Output Ranges</option> | |
<option value="lists">Output Lists</option> | |
</select> | |
</label> | |
<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 = 1 | |
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 | |
} | |
} | |
const outputType = document.getElementById('outputType').value | |
let out = '' | |
let maxTracks = {} | |
if (outputType === 'lists') { | |
// calculate max track number for each medium | |
const tracks = Object.values(parsed).map(at => Object.values(at)).flat().map(ts => [...ts]).flat() | |
for (const [medium, track] of tracks) { | |
if (!maxTracks[medium] || maxTracks[medium] < track) { | |
maxTracks[medium] = track | |
} | |
} | |
} | |
for (const [role, artistsAndTracks] of Object.entries(parsed)) { | |
out += `${role}:\n` | |
switch (outputType) { | |
case 'ranges': { | |
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' | |
} | |
break | |
} | |
case 'lists': { | |
const tracksToArtists = {} | |
for (const [artist, unsortedTracks] of Object.entries(artistsAndTracks)) { | |
for (const t of unsortedTracks) { | |
const tf = `${t[0]}.${t[1]}` | |
if (!tracksToArtists[tf]) { | |
tracksToArtists[tf] = [] | |
} | |
tracksToArtists[tf].push(artist) | |
} | |
} | |
for (const [medium, maxTrack] of Object.entries(maxTracks)) { | |
for (let i = 1; i <= maxTrack; i++) { | |
const tf = `${medium}.${i}` | |
out += `${tf} ` | |
if (tracksToArtists[tf]) { | |
out += tracksToArtists[tf].join(', ') | |
} else { | |
out += '---' | |
} | |
out += '\n' | |
} | |
} | |
break | |
} | |
} | |
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) | |
document.getElementById('outputType').addEventListener('change', parse) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment