Compare commits

..

4 commits

Author SHA1 Message Date
6cf89b0d20 Implement download button 2025-03-12 20:08:58 -06:00
62b01f0d94 Disable format tag 2025-03-12 19:38:46 -06:00
991d69ed96 Rework individual song download 2025-03-12 19:33:53 -06:00
4ece9041a6 Change CORS url 2025-03-11 20:15:48 -06:00
5 changed files with 96 additions and 20 deletions

View file

@ -1,9 +1,10 @@
import { useEffect, useState } from 'react'
import { getDownloadUrl, getTracks } from 'utils/search'
import { getDownloadUrl, getTracks, triggerDownload } from 'utils/search'
import type { AlbumRow, TrackRow } from 'utils/types'
import Loader from './Loader'
import ClipboardBtn from './ClipboardBtn'
import { AlbumDLBtn } from './AlbumDLBtn'
export default function Album(props: { album: AlbumRow; lastReqRef: React.RefObject<number> }) {
const { album, lastReqRef } = props
@ -15,6 +16,7 @@ export default function Album(props: { album: AlbumRow; lastReqRef: React.RefObj
<h3>{album.name}</h3>
<div className='btn-container'>
<ClipboardBtn albumUrl={album.url} lastReqRef={lastReqRef} />
<AlbumDLBtn album={album} lastReqRef={lastReqRef} />
</div>
</div>
<TrackList url={album.url} open={open} lastReqRef={lastReqRef} />
@ -58,8 +60,8 @@ function TrackList(props: { url: string; open: boolean; lastReqRef: React.RefObj
<span className='track-number'>{(i + 1).toString().padStart(2, '0').slice(-2)}. </span>
<span>{t.name}</span>
</span>
<span className='format-tag'>FLAC/MP3</span>
<TrackDownload url={t.url} lastReqRef={lastReqRef} />
{/* <span className='format-tag'>{t.format}</span> */}
<TrackDownload track={t} lastReqRef={lastReqRef} />
</div>
))}
</div>
@ -68,27 +70,38 @@ function TrackList(props: { url: string; open: boolean; lastReqRef: React.RefObj
)
}
function TrackDownload(props: { url: string; lastReqRef: React.RefObject<number> }) {
const { url, lastReqRef } = props
function TrackDownload(props: { track: TrackRow; lastReqRef: React.RefObject<number> }) {
const { track, lastReqRef } = props
const [downloadUrl, setDownloadUrl] = useState<string>()
const [loading, setLoading] = useState(false)
async function fetchUrl() {
try {
setLoading(true)
const dlUrl = await getDownloadUrl(url, lastReqRef)
const dlUrl = await getDownloadUrl(track.url, lastReqRef)
setDownloadUrl(dlUrl)
await downloadTrack(dlUrl)
} finally {
setLoading(false)
}
}
return downloadUrl ? (
<a className='download-btn' href={downloadUrl} target='_blank' rel='noopener noreferrer' download>
Download
</a>
) : (
<button className='download-btn' disabled={loading} onClick={fetchUrl}>
async function downloadTrack(dlUrl: string) {
if (!dlUrl) return
const dlBlob = await (await fetch(dlUrl)).blob()
const fileURL = window.URL.createObjectURL(dlBlob)
triggerDownload(fileURL, track.fileName)
}
return (
<button
className='download-btn'
disabled={loading}
onClick={downloadUrl ? () => downloadTrack(downloadUrl) : fetchUrl}
>
{loading ? <Loader show /> : 'Download'}
</button>
)

View file

@ -0,0 +1,29 @@
import { useState } from 'react'
import { downloadAlbum } from 'utils/search'
import type { AlbumRow } from 'utils/types'
import Loader from './Loader'
export function AlbumDLBtn(props: { album: AlbumRow; lastReqRef: React.RefObject<number> }) {
const { album, lastReqRef } = props
const [loading, setLoading] = useState(false)
function handleClick(ev: React.MouseEvent<HTMLButtonElement>) {
ev.stopPropagation()
setLoading(true)
downloadAlbum(album, lastReqRef).finally(() => setLoading(false))
}
return (
<button className='download-btn' onClick={handleClick} disabled={loading}>
{loading ? (
<div className='loader-container'>
<Loader show />
</div>
) : (
<span> Download album</span>
)}
</button>
)
}

View file

@ -99,6 +99,7 @@ import Downloader from '../components/Downloader'
flex-shrink: 0;
display: flex;
column-gap: 4px;
margin-left: 5px;
}
.track {
display: flex;
@ -114,10 +115,10 @@ import Downloader from '../components/Downloader'
.format-tag {
background-color: #4a4a4a;
color: white;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.85em;
margin-right: 10px;
margin: 10px;
text-transform: uppercase;
}
.download-btn {
background-color: #28a745;

View file

@ -1,16 +1,20 @@
import * as cheerio from 'cheerio'
// import JSZipUtils from 'jszip-utils'
import JSZip from 'jszip'
import toast from 'react-hot-toast'
import { USER_AGENTS } from './consts'
import type { AlbumRow } from './types'
import type { AlbumRow, TrackRow } from './types'
function getRandom(list: any[]) {
return list[Math.floor(Math.random() * list.length)]
}
export const getCors = (url: string) => `https://cors.squid.wtf/${url}`
export function politeFetch(url: string, lastReqRef: React.RefObject<number>) {
const elapsed = Date.now() - lastReqRef.current
const corsUrl = `https://cors.chitowarlock.com/${url}`
const corsUrl = getCors(url)
const headers = {
'User-Agent': getRandom(USER_AGENTS),
@ -57,7 +61,7 @@ export async function searchAlbums(query: string, lastReqRef: React.RefObject<nu
export async function getTracks(url: string, lastReqRef: React.RefObject<number>) {
const albumHtml = await (await politeFetch(url, lastReqRef)).text()
const $ = cheerio.load(albumHtml)
const tracks: { name: string; url: string; format: string }[] = []
const tracks: TrackRow[] = []
$('table#songlist tr:has(a[href$=".mp3"]), table#songlist tr:has(a[href$=".flac"])').each((index, row) => {
const link = $(row).find('a[href$=".mp3"], a[href$=".flac"]')
@ -66,11 +70,13 @@ export async function getTracks(url: string, lastReqRef: React.RefObject<number>
const trackUrl = link.attr('href')!.startsWith('http')
? link.attr('href')!
: `https://downloads.khinsider.com${link.attr('href')}`
const trackPath = trackUrl.split('/')
tracks.push({
name: link.first().text().trim(),
url: trackUrl,
format: link.attr('href')!.toLowerCase().endsWith('.flac') ? 'flac' : 'mp3'
format: link.attr('href')!.toLowerCase().endsWith('.flac') ? 'flac' : 'mp3',
fileName: decodeURI(decodeURI(trackPath[trackPath.length - 1]))
})
})
@ -96,13 +102,15 @@ export async function getDownloadUrl(trackPageUrl: string, lastReqRef: React.Ref
const flacLinks = audioLinks.filter((link) => link.endsWith('.flac'))
if (flacLinks.length > 0) {
return flacLinks[0]
return getCors(flacLinks[0])
}
const mp3Links = audioLinks.filter((link) => link.endsWith('.mp3'))
if (mp3Links.length > 0) {
return mp3Links[0]
return getCors(mp3Links[0])
}
throw new Error('Failed to fetch download url')
}
export async function copyDownloadUrls(albumUrl: string, lastReqRef: React.RefObject<number>) {
@ -113,3 +121,27 @@ export async function copyDownloadUrls(albumUrl: string, lastReqRef: React.RefOb
await navigator.clipboard.writeText(filterUrl.join('\n'))
toast.success('Copied links to the clipboard!')
}
export function triggerDownload(url: string, fileName: string) {
const fileLink = document.createElement('a')
fileLink.href = url
fileLink.setAttribute('download', fileName)
document.body.appendChild(fileLink)
fileLink.click()
fileLink.remove()
}
export async function downloadAlbum(album: AlbumRow, lastReqRef: React.RefObject<number>) {
const trackList = await getTracks(album.url, lastReqRef)
const newZip = new JSZip()
const downloadList = await Promise.all(
trackList.map(async (t) => ({ ...t, blob: await (await fetch(await getDownloadUrl(t.url, lastReqRef))).blob() }))
)
downloadList.forEach((d) => {
newZip.file(d.fileName, d.blob)
})
const zipBlob = window.URL.createObjectURL(await newZip.generateAsync({ type: 'blob' }))
triggerDownload(zipBlob, `${album.name}.zip`)
}

View file

@ -7,4 +7,5 @@ export interface TrackRow {
name: string
url: string
format: string
fileName: string
}