added comments

This commit is contained in:
2025-10-11 01:07:47 -05:00
parent 95c1a568db
commit de58099ac0

52
main.ts
View File

@@ -13,12 +13,15 @@ const REGEX_ALBUMTITLE = /^(.*?) MP3 - Download/i
const REGEX_UNSAFEFORFILE = /[^a-z0-9\-_=+,.()\[\]{} ]/gi const REGEX_UNSAFEFORFILE = /[^a-z0-9\-_=+,.()\[\]{} ]/gi
async function main() { async function main() {
// deno main.ts <playlistURL> [<downloadPath>] [sync]
const playlistURL = argv[2] const playlistURL = argv[2]
const downloadPath = argv[3] || '.' const downloadPath = argv[3] || '.'
const runSync = argv[4]?.toLowerCase() == 'sync' const runSync = argv[4]?.toLowerCase() == 'sync'
// load all song details
const playlist = await fetchPlaylist(playlistURL) const playlist = await fetchPlaylist(playlistURL)
// if sync, download one at a time
if (runSync) { if (runSync) {
for (const song of playlist) { for (const song of playlist) {
await downloadSong(song, downloadPath || '.') await downloadSong(song, downloadPath || '.')
@@ -35,11 +38,12 @@ async function fetchPlaylist(url: string): Promise<PlaylistSongData[]> {
throw `unaccepted url ${url}` throw `unaccepted url ${url}`
} }
// load the playlist page's dom
const req = await fetch(url) const req = await fetch(url)
const text = await req.text() const text = await req.text()
const $ = cheerio.load(text) const $ = cheerio.load(text)
// get the album name from the page title
const title = const title =
$.extract({ $.extract({
title: 'title', title: 'title',
@@ -50,6 +54,7 @@ async function fetchPlaylist(url: string): Promise<PlaylistSongData[]> {
} }
const albumName = titleMatch[1] const albumName = titleMatch[1]
// parse all rows in playlist
const columns = getPlaylistTableHeaders($) const columns = getPlaylistTableHeaders($)
const rows = getPlaylistRows($, columns, albumName) const rows = getPlaylistRows($, columns, albumName)
@@ -73,13 +78,16 @@ interface PlaylistSongData {
} }
function getPlaylistTableHeaders($: cheerio.CheerioAPI): PlaylistTableColumns { function getPlaylistTableHeaders($: cheerio.CheerioAPI): PlaylistTableColumns {
// get table header row
const header = $('#songlist tr#songlist_header') const header = $('#songlist tr#songlist_header')
const headerCells = header.extract({ const headerCells = header.extract({
cells: ['th'], cells: ['th'],
}) })
// get the index for specific columns
const indexes = headerCells.cells.reduce( const indexes = headerCells.cells.reduce(
(p, c, i) => { (p, c, i) => {
// check the string content of the current cell
switch (c.toLocaleLowerCase()) { switch (c.toLocaleLowerCase()) {
case 'cd': case 'cd':
p.cd = i p.cd = i
@@ -92,6 +100,7 @@ function getPlaylistTableHeaders($: cheerio.CheerioAPI): PlaylistTableColumns {
} }
return p return p
}, },
// default values
{ {
name: -1, name: -1,
track: -1, track: -1,
@@ -114,6 +123,7 @@ function getPlaylistRows(
const rows = $('#songlist tr:not(#songlist_header):not(#songlist_footer)') const rows = $('#songlist tr:not(#songlist_header):not(#songlist_footer)')
const rowsData: PlaylistSongData[] = [] const rowsData: PlaylistSongData[] = []
// loop through each song in table
rows.each((_, rowEl) => { rows.each((_, rowEl) => {
const row = cheerio.load(rowEl) const row = cheerio.load(rowEl)
const rowData: PlaylistSongData = { const rowData: PlaylistSongData = {
@@ -124,6 +134,7 @@ function getPlaylistRows(
cd: -1, cd: -1,
} }
// prase values from row
rowData.url = rowData.url =
row.extract({ row.extract({
url: { url: {
@@ -162,6 +173,7 @@ function getPlaylistRows(
} }
async function downloadSong(song: PlaylistSongData, location: string) { async function downloadSong(song: PlaylistSongData, location: string) {
// get full url
let url = song.url let url = song.url
if (!/^http/i.test(url)) { if (!/^http/i.test(url)) {
url = 'https://downloads.khinsider.com' + url url = 'https://downloads.khinsider.com' + url
@@ -170,10 +182,12 @@ async function downloadSong(song: PlaylistSongData, location: string) {
throw `unaccepted url ${url}` throw `unaccepted url ${url}`
} }
// load download page
const resp = await fetch(url) const resp = await fetch(url)
const text = await resp.text() const text = await resp.text()
const $ = cheerio.load(text) const $ = cheerio.load(text)
// extract the flac download link
const flacUrl = $.extract({ const flacUrl = $.extract({
url: { url: {
selector: '#pageContent a[href*="flac"]', selector: '#pageContent a[href*="flac"]',
@@ -185,11 +199,15 @@ async function downloadSong(song: PlaylistSongData, location: string) {
throw `can't find download link for ${url}` throw `can't find download link for ${url}`
} }
// get the file and path to save the files
const { pathname, fullpathname } = pathFor(location, song) const { pathname, fullpathname } = pathFor(location, song)
// ensure folder exists
if (!existsSync(pathname)) { if (!existsSync(pathname)) {
await mkdir(pathname) await mkdir(pathname)
} }
// skip file if it exists
if (existsSync(fullpathname)) { if (existsSync(fullpathname)) {
console.log(`skipping file already exists ${fullpathname}`) console.log(`skipping file already exists ${fullpathname}`)
} }
@@ -197,9 +215,9 @@ async function downloadSong(song: PlaylistSongData, location: string) {
console.log(`downloading ${fullpathname}`) console.log(`downloading ${fullpathname}`)
console.log(` from ${flacUrl}`) console.log(` from ${flacUrl}`)
// download the file
const songresp = await fetch(flacUrl) const songresp = await fetch(flacUrl)
const songblob = await songresp.arrayBuffer() const songblob = await songresp.arrayBuffer()
return writeFile(fullpathname, toBuffer(songblob)) return writeFile(fullpathname, toBuffer(songblob))
} }
@@ -209,6 +227,7 @@ interface SongPath {
} }
function pathFor(location: string, song: PlaylistSongData): SongPath { function pathFor(location: string, song: PlaylistSongData): SongPath {
// clean strings for file paths
const albumname = song.album.replace(REGEX_UNSAFEFORFILE, '') const albumname = song.album.replace(REGEX_UNSAFEFORFILE, '')
const songname = song.name.replace(REGEX_UNSAFEFORFILE, '') const songname = song.name.replace(REGEX_UNSAFEFORFILE, '')
@@ -216,6 +235,30 @@ function pathFor(location: string, song: PlaylistSongData): SongPath {
const track = song.track >= 0 ? song.track + '.' : '' const track = song.track >= 0 ? song.track + '.' : ''
const separator = song.cd >= 0 || song.track >= 0 ? ' ' : '' const separator = song.cd >= 0 || song.track >= 0 ? ' ' : ''
const filename = `${cd}${track}${separator}${songname}.flac` const filename = `${cd}${track}${separator}${songname}.flac`
/*
for example
song = {
songname: 'song',
track: 1,
cd: 1,
}
then filename = '1.1. song'
song = {
songname: 'song',
track: 1,
cd: -1,
}
then filename = '1. song'
song = {
songname: 'song',
track: -1,
cd: -1,
}
then filename = 'song'
*/
const pathname = path.resolve(location, albumname) const pathname = path.resolve(location, albumname)
const fullpathname = path.resolve(pathname, filename) const fullpathname = path.resolve(pathname, filename)
@@ -226,7 +269,8 @@ function pathFor(location: string, song: PlaylistSongData): SongPath {
} }
} }
function toBuffer(arrayBuffer: ArrayBuffer) { // convert ArrayBuffer to Buffer<ArrayBuffer>
function toBuffer(arrayBuffer: ArrayBuffer): Buffer<ArrayBuffer> {
const buffer = Buffer.alloc(arrayBuffer.byteLength) const buffer = Buffer.alloc(arrayBuffer.byteLength)
const view = new Uint8Array(arrayBuffer) const view = new Uint8Array(arrayBuffer)
for (let i = 0; i < buffer.length; ++i) { for (let i = 0; i < buffer.length; ++i) {