Compare commits
No commits in common. "6cf89b0d20c99a23983e7d62ec4169eaef053404" and "c8cd8c724fe5a6590390a413b439ecec59f185cd" have entirely different histories.
6cf89b0d20
...
c8cd8c724f
5 changed files with 20 additions and 96 deletions
|
|
@ -1,10 +1,9 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { getDownloadUrl, getTracks, triggerDownload } from 'utils/search'
|
import { getDownloadUrl, getTracks } from 'utils/search'
|
||||||
import type { AlbumRow, TrackRow } from 'utils/types'
|
import type { AlbumRow, TrackRow } from 'utils/types'
|
||||||
import Loader from './Loader'
|
import Loader from './Loader'
|
||||||
import ClipboardBtn from './ClipboardBtn'
|
import ClipboardBtn from './ClipboardBtn'
|
||||||
import { AlbumDLBtn } from './AlbumDLBtn'
|
|
||||||
|
|
||||||
export default function Album(props: { album: AlbumRow; lastReqRef: React.RefObject<number> }) {
|
export default function Album(props: { album: AlbumRow; lastReqRef: React.RefObject<number> }) {
|
||||||
const { album, lastReqRef } = props
|
const { album, lastReqRef } = props
|
||||||
|
|
@ -16,7 +15,6 @@ export default function Album(props: { album: AlbumRow; lastReqRef: React.RefObj
|
||||||
<h3>{album.name}</h3>
|
<h3>{album.name}</h3>
|
||||||
<div className='btn-container'>
|
<div className='btn-container'>
|
||||||
<ClipboardBtn albumUrl={album.url} lastReqRef={lastReqRef} />
|
<ClipboardBtn albumUrl={album.url} lastReqRef={lastReqRef} />
|
||||||
<AlbumDLBtn album={album} lastReqRef={lastReqRef} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TrackList url={album.url} open={open} lastReqRef={lastReqRef} />
|
<TrackList url={album.url} open={open} lastReqRef={lastReqRef} />
|
||||||
|
|
@ -60,8 +58,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 className='track-number'>{(i + 1).toString().padStart(2, '0').slice(-2)}. </span>
|
||||||
<span>{t.name}</span>
|
<span>{t.name}</span>
|
||||||
</span>
|
</span>
|
||||||
{/* <span className='format-tag'>{t.format}</span> */}
|
<span className='format-tag'>FLAC/MP3</span>
|
||||||
<TrackDownload track={t} lastReqRef={lastReqRef} />
|
<TrackDownload url={t.url} lastReqRef={lastReqRef} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -70,38 +68,27 @@ function TrackList(props: { url: string; open: boolean; lastReqRef: React.RefObj
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TrackDownload(props: { track: TrackRow; lastReqRef: React.RefObject<number> }) {
|
function TrackDownload(props: { url: string; lastReqRef: React.RefObject<number> }) {
|
||||||
const { track, lastReqRef } = props
|
const { url, lastReqRef } = props
|
||||||
const [downloadUrl, setDownloadUrl] = useState<string>()
|
const [downloadUrl, setDownloadUrl] = useState<string>()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
async function fetchUrl() {
|
async function fetchUrl() {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const dlUrl = await getDownloadUrl(track.url, lastReqRef)
|
const dlUrl = await getDownloadUrl(url, lastReqRef)
|
||||||
|
|
||||||
setDownloadUrl(dlUrl)
|
setDownloadUrl(dlUrl)
|
||||||
await downloadTrack(dlUrl)
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadTrack(dlUrl: string) {
|
return downloadUrl ? (
|
||||||
if (!dlUrl) return
|
<a className='download-btn' href={downloadUrl} target='_blank' rel='noopener noreferrer' download>
|
||||||
|
Download
|
||||||
const dlBlob = await (await fetch(dlUrl)).blob()
|
</a>
|
||||||
const fileURL = window.URL.createObjectURL(dlBlob)
|
) : (
|
||||||
|
<button className='download-btn' disabled={loading} onClick={fetchUrl}>
|
||||||
triggerDownload(fileURL, track.fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className='download-btn'
|
|
||||||
disabled={loading}
|
|
||||||
onClick={downloadUrl ? () => downloadTrack(downloadUrl) : fetchUrl}
|
|
||||||
>
|
|
||||||
{loading ? <Loader show /> : 'Download'}
|
{loading ? <Loader show /> : 'Download'}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -99,7 +99,6 @@ import Downloader from '../components/Downloader'
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 4px;
|
column-gap: 4px;
|
||||||
margin-left: 5px;
|
|
||||||
}
|
}
|
||||||
.track {
|
.track {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -115,10 +114,10 @@ import Downloader from '../components/Downloader'
|
||||||
.format-tag {
|
.format-tag {
|
||||||
background-color: #4a4a4a;
|
background-color: #4a4a4a;
|
||||||
color: white;
|
color: white;
|
||||||
|
padding: 3px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
margin: 10px;
|
margin-right: 10px;
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
.download-btn {
|
.download-btn {
|
||||||
background-color: #28a745;
|
background-color: #28a745;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
import * as cheerio from 'cheerio'
|
import * as cheerio from 'cheerio'
|
||||||
// import JSZipUtils from 'jszip-utils'
|
|
||||||
import JSZip from 'jszip'
|
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
import { USER_AGENTS } from './consts'
|
import { USER_AGENTS } from './consts'
|
||||||
import type { AlbumRow, TrackRow } from './types'
|
import type { AlbumRow } from './types'
|
||||||
|
|
||||||
function getRandom(list: any[]) {
|
function getRandom(list: any[]) {
|
||||||
return list[Math.floor(Math.random() * list.length)]
|
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>) {
|
export function politeFetch(url: string, lastReqRef: React.RefObject<number>) {
|
||||||
const elapsed = Date.now() - lastReqRef.current
|
const elapsed = Date.now() - lastReqRef.current
|
||||||
const corsUrl = getCors(url)
|
const corsUrl = `https://cors.chitowarlock.com/${url}`
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'User-Agent': getRandom(USER_AGENTS),
|
'User-Agent': getRandom(USER_AGENTS),
|
||||||
|
|
@ -61,7 +57,7 @@ export async function searchAlbums(query: string, lastReqRef: React.RefObject<nu
|
||||||
export async function getTracks(url: string, lastReqRef: React.RefObject<number>) {
|
export async function getTracks(url: string, lastReqRef: React.RefObject<number>) {
|
||||||
const albumHtml = await (await politeFetch(url, lastReqRef)).text()
|
const albumHtml = await (await politeFetch(url, lastReqRef)).text()
|
||||||
const $ = cheerio.load(albumHtml)
|
const $ = cheerio.load(albumHtml)
|
||||||
const tracks: TrackRow[] = []
|
const tracks: { name: string; url: string; format: string }[] = []
|
||||||
|
|
||||||
$('table#songlist tr:has(a[href$=".mp3"]), table#songlist tr:has(a[href$=".flac"])').each((index, row) => {
|
$('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"]')
|
const link = $(row).find('a[href$=".mp3"], a[href$=".flac"]')
|
||||||
|
|
@ -70,13 +66,11 @@ export async function getTracks(url: string, lastReqRef: React.RefObject<number>
|
||||||
const trackUrl = link.attr('href')!.startsWith('http')
|
const trackUrl = link.attr('href')!.startsWith('http')
|
||||||
? link.attr('href')!
|
? link.attr('href')!
|
||||||
: `https://downloads.khinsider.com${link.attr('href')}`
|
: `https://downloads.khinsider.com${link.attr('href')}`
|
||||||
const trackPath = trackUrl.split('/')
|
|
||||||
|
|
||||||
tracks.push({
|
tracks.push({
|
||||||
name: link.first().text().trim(),
|
name: link.first().text().trim(),
|
||||||
url: trackUrl,
|
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]))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -102,15 +96,13 @@ export async function getDownloadUrl(trackPageUrl: string, lastReqRef: React.Ref
|
||||||
|
|
||||||
const flacLinks = audioLinks.filter((link) => link.endsWith('.flac'))
|
const flacLinks = audioLinks.filter((link) => link.endsWith('.flac'))
|
||||||
if (flacLinks.length > 0) {
|
if (flacLinks.length > 0) {
|
||||||
return getCors(flacLinks[0])
|
return flacLinks[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
const mp3Links = audioLinks.filter((link) => link.endsWith('.mp3'))
|
const mp3Links = audioLinks.filter((link) => link.endsWith('.mp3'))
|
||||||
if (mp3Links.length > 0) {
|
if (mp3Links.length > 0) {
|
||||||
return getCors(mp3Links[0])
|
return mp3Links[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Failed to fetch download url')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function copyDownloadUrls(albumUrl: string, lastReqRef: React.RefObject<number>) {
|
export async function copyDownloadUrls(albumUrl: string, lastReqRef: React.RefObject<number>) {
|
||||||
|
|
@ -121,27 +113,3 @@ export async function copyDownloadUrls(albumUrl: string, lastReqRef: React.RefOb
|
||||||
await navigator.clipboard.writeText(filterUrl.join('\n'))
|
await navigator.clipboard.writeText(filterUrl.join('\n'))
|
||||||
toast.success('Copied links to the clipboard!')
|
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`)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,4 @@ export interface TrackRow {
|
||||||
name: string
|
name: string
|
||||||
url: string
|
url: string
|
||||||
format: string
|
format: string
|
||||||
fileName: string
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue