init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
8
deno.json
Normal file
8
deno.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"dev": "deno run --watch main.ts"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@std/assert": "jsr:@std/assert@1"
|
||||||
|
}
|
||||||
|
}
|
||||||
95
main.ts
Normal file
95
main.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
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))
|
||||||
29
package-lock.json
generated
Normal file
29
package-lock.json
generated
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "khinsider downloads",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.5.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "24.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
|
||||||
|
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
||||||
|
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user