added comments
This commit is contained in:
52
main.ts
52
main.ts
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user