diff --git a/src/components/Album.tsx b/src/components/Album.tsx index 95e508a..512c38e 100644 --- a/src/components/Album.tsx +++ b/src/components/Album.tsx @@ -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 }) { const { album, lastReqRef } = props @@ -15,6 +16,7 @@ export default function Album(props: { album: AlbumRow; lastReqRef: React.RefObj

{album.name}

+
@@ -58,8 +60,8 @@ function TrackList(props: { url: string; open: boolean; lastReqRef: React.RefObj {(i + 1).toString().padStart(2, '0').slice(-2)}. {t.name} - FLAC/MP3 - + {/* {t.format} */} + ))} @@ -68,27 +70,38 @@ function TrackList(props: { url: string; open: boolean; lastReqRef: React.RefObj ) } -function TrackDownload(props: { url: string; lastReqRef: React.RefObject }) { - const { url, lastReqRef } = props +function TrackDownload(props: { track: TrackRow; lastReqRef: React.RefObject }) { + const { track, lastReqRef } = props const [downloadUrl, setDownloadUrl] = useState() 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 ? ( - - Download - - ) : ( - ) diff --git a/src/components/AlbumDLBtn.tsx b/src/components/AlbumDLBtn.tsx new file mode 100644 index 0000000..63dd5a2 --- /dev/null +++ b/src/components/AlbumDLBtn.tsx @@ -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 }) { + const { album, lastReqRef } = props + const [loading, setLoading] = useState(false) + + function handleClick(ev: React.MouseEvent) { + ev.stopPropagation() + setLoading(true) + downloadAlbum(album, lastReqRef).finally(() => setLoading(false)) + } + + return ( + + ) +} diff --git a/src/pages/index.astro b/src/pages/index.astro index 0f8d658..1c9ec84 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -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; diff --git a/src/utils/search.ts b/src/utils/search.ts index f4a067e..29dd62e 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -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) { 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) { 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 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) { @@ -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) { + 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`) +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 2142fa8..c04a80f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -7,4 +7,5 @@ export interface TrackRow { name: string url: string format: string + fileName: string }