|
const cue = []; |
|
const pageTitle = $("div#pageTitle").text().trim(); |
|
const parts = pageTitle |
|
.split(/ [@-] /, 2) |
|
.map((p) => p.trim()); |
|
const date = parts[1].substr(parts[1].length - 10); |
|
const title = parts[1].substr(0, parts[1].length - 10).trim(); |
|
const filename = `${date} ${pageTitle.substr(0, pageTitle.length-10).trim()}`; |
|
cue.push( |
|
`REM Generated by https://gist.github.com/elliotchance/86ff315d552df269e97ff52b129e02a7`, |
|
`PERFORMER "${parts[0]}"`, |
|
`TITLE "${date} ${title}"`, |
|
`FILE "${filename}.mp3" MP3` |
|
); |
|
|
|
function parseTimeToSeconds(timeStr) { |
|
const parts = timeStr.split(':').map(Number); |
|
if (parts.length === 3) { |
|
const [hours, minutes, seconds] = parts; |
|
return hours * 3600 + minutes * 60 + seconds; |
|
} else if (parts.length === 2) { |
|
const [minutes, seconds] = parts; |
|
return minutes * 60 + seconds; |
|
} else { |
|
throw new Error(`Invalid time format "${timeStr}"`); |
|
} |
|
} |
|
|
|
const totalDuration = $('li.tBtn').text().match(/\[((\d+:)?(\d+):(\d+))\]/) || |
|
['', prompt("Duration is unknown. How long is the mix? (MM:SS)")]; |
|
const trackStarts = []; |
|
$("div.bItm").each((i, t) => { |
|
const text = $(t).text(); |
|
if (text.includes('correct cue time is')) { |
|
const startTime = $(t).find('.italic').text().trim(); |
|
trackStarts[trackStarts.length-1] = parseTimeToSeconds(startTime); |
|
} else if (!text.match(/^\s*\d+\s+/)) { |
|
// Avoid (trimmed) - these are not tracks: |
|
// 1:16:31Estiva - Peppa[25-10-22 19:04:00]akselek |
|
// w/ 1:50:38 Estiva - Via Infinita COLORIZE (ENHANCED) |
|
// track wasn't played[25-10-20 23:34:30]biscram[poll:0/1/0] |
|
return |
|
} else { |
|
const startTime = $(t).find(".cue:last-of-type").text(); |
|
trackStarts.push(startTime ? parseTimeToSeconds(startTime) : 0); |
|
} |
|
}) |
|
|
|
function fillMissingTimes(times, maxTime) { |
|
const result = [...times]; |
|
let lastKnownTime = 0; |
|
let lastKnownIndex = 0; |
|
|
|
// Collect indices of known (non-zero) times |
|
const known = times |
|
.map((t, i) => (t > 0 ? [i, t] : null)) |
|
.filter(Boolean); |
|
|
|
// If no known times, evenly distribute |
|
if (known.length === 0) { |
|
const step = Math.floor(maxTime / times.length); |
|
return times.map((_, i) => i * step); |
|
} |
|
|
|
// Fill between known times |
|
for (let k = 0; k < known.length; k++) { |
|
const [i, t] = known[k]; |
|
const gap = i - lastKnownIndex; |
|
|
|
if (gap > 1) { |
|
const step = (t - lastKnownTime) / gap; |
|
for (let j = 1; j < gap; j++) { |
|
result[lastKnownIndex + j] = Math.round(lastKnownTime + step * j); |
|
} |
|
} |
|
|
|
lastKnownTime = t; |
|
lastKnownIndex = i; |
|
} |
|
|
|
// Fill after the last known time up to maxTime |
|
if (lastKnownIndex < times.length - 1) { |
|
const remaining = times.length - lastKnownIndex - 1; |
|
const step = (maxTime - lastKnownTime) / (remaining + 1); |
|
for (let j = 1; j <= remaining; j++) { |
|
result[lastKnownIndex + j] = Math.round(lastKnownTime + step * j); |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
|
|
const fixedTrackStarts = fillMissingTimes(trackStarts, parseTimeToSeconds(totalDuration[1])); |
|
|
|
let trackNumber = 1; |
|
$("div.bItm.tlpItem").each((_, t) => { |
|
if (!$(t).text().match(/^\s*\d+\s+/)) { |
|
return |
|
} |
|
const parts = ( |
|
$(t).find("meta[itemprop=name]").attr("content") || $(t).find('span.trackValue').text() |
|
).split(" - ", 2); |
|
const h = Math.floor(fixedTrackStarts[trackNumber-1] / 60); |
|
const m = fixedTrackStarts[i] % 60; |
|
const d = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; |
|
cue.push( |
|
` TRACK ${String(trackNumber).padStart(2, "0")} AUDIO`, |
|
` PERFORMER "${parts[0].trim()}"`, |
|
` TITLE "${parts[1].trim()}"`, |
|
` INDEX 01 ${d}:00`, |
|
); |
|
++trackNumber; |
|
}); |
|
|
|
(() => { |
|
const content = cue.join("\n") + "\n"; |
|
console.log(content); |
|
|
|
const url = URL.createObjectURL(new Blob([content], { type: "application/x-cue" })) |
|
const a = document.createElement("a"); |
|
a.href = url; |
|
a.download = `${filename}.cue`; |
|
setTimeout(() => a.click(), 0); // ensures Chrome treats it as user action |
|
setTimeout(() => URL.revokeObjectURL(url), 1000); |
|
})(); |