Files
khinsider-downloads/main.ts
2025-09-26 19:11:03 -05:00

96 lines
2.8 KiB
TypeScript

import { existsSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import path from 'node:path'
import {argv} from 'node:process'
const REGEX_PLAYLISTURL = /^https:\/\/downloads\.khinsider\.com\/game-soundtracks\/album\/[^\/]+\/?$/i
const REGEX_SONGURL = /^https:\/\/downloads\.khinsider\.com\/game-soundtracks\/album\/[^\/]+\/[^\/]+\/?$/i
const REGEX_FILEPATHPARSE = /^\/soundtracks\/([^\/]+)\/[^\/]+\/([^\/]+)$/i
const REGEX_PLAYLIST_SONGHREF = /<td class="playlistDownloadSong"><a href="(.*?)">/gi
const REGEX_SONG_FILEHREF = /<a\s+href=['"](.*?\.flac)['"]><span\s+class=['"]songDownloadLink/i
async function main() {
const playlistURL = argv[2]
const downloadPath = argv[3] || '.'
const runSync = argv[4]?.toLowerCase() == 'sync'
const playlist = await fetchPlaylist(playlistURL)
if (runSync) {
for (const song of playlist) {
await downloadSong(song, downloadPath || '.')
}
} else {
await Promise.all(playlist.map(song => downloadSong(song, downloadPath || '.')))
}
}
async function fetchPlaylist(url: string): Promise<string[]> {
if (!REGEX_PLAYLISTURL.test(url)) {
throw `unaccepted url ${url}`
}
const resp = await fetch(url)
const text = await resp.text()
const matches = text.matchAll(REGEX_PLAYLIST_SONGHREF)
const matchesArr = [...matches].map(match => match[1])
console.log(`downloading ${matchesArr.length} songs`)
return matchesArr
}
async function downloadSong(url: string, location: string) {
if (!/^http/i.test(url)) {
url = 'https://downloads.khinsider.com' + url
}
if (!REGEX_SONGURL.test(url)) {
throw `unaccepted url ${url}`
}
let resp = await fetch(url)
let text = await resp.text()
let match = text.match(REGEX_SONG_FILEHREF)
if (!match) {
throw `can't find download link for ${url}`
}
const songurl = new URL(match[1])
const songurlmatch = REGEX_FILEPATHPARSE.exec(songurl.pathname)
if (!songurlmatch) {
throw `can't find folder and filename for ${songurl}`
}
const foldername = decodeURIComponent(songurlmatch[1])
const filename = decodeURIComponent(songurlmatch[2])
const pathname = path.resolve(location, foldername)
const fullpathname = path.resolve(pathname, filename)
if (!existsSync(pathname)) {
await mkdir(pathname)
}
if (existsSync(fullpathname)) {
console.log(`skipping file already exists ${fullpathname}`)
}
console.log(`downloading ${fullpathname}`)
const songresp = await fetch(songurl)
const songblob = await songresp.arrayBuffer()
return writeFile(fullpathname, toBuffer(songblob))
}
function toBuffer(arrayBuffer: ArrayBuffer) {
const buffer = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i];
}
return buffer;
}
main().catch(e => console.error(e))