chore: Clean slate
This commit is contained in:
parent
9bcba4caaa
commit
48c0059860
4 changed files with 0 additions and 286 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -103,7 +103,6 @@ dist
|
|||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
|
61
src/index.ts
61
src/index.ts
|
@ -1,61 +0,0 @@
|
|||
import express from 'express';
|
||||
import bodyParser from "body-parser";
|
||||
import QueueManager from './queueManager';
|
||||
|
||||
const port = 3000;
|
||||
const api = '/api/v1';
|
||||
|
||||
const app = express();
|
||||
const qm = new QueueManager();
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
|
||||
app.get(api + '/queue', (req, res) => {
|
||||
res.json(qm.getQueue().map(qi => qi.album.href));
|
||||
});
|
||||
|
||||
app.post(api + '/queue', (req, res) => {
|
||||
const album: string = req.body.album;
|
||||
|
||||
if (!album) {
|
||||
res.status(400).send('No album URL provided'); // Bad request
|
||||
return;
|
||||
}
|
||||
|
||||
const result: boolean | string = qm.addToQueue(album);
|
||||
if (result === true) {
|
||||
res.status(201).send('Album added to queue'); // Created
|
||||
} else {
|
||||
res.status(409).send(result); // Conflict
|
||||
}
|
||||
});
|
||||
|
||||
app.delete(api + '/queue', (req, res) => {
|
||||
qm.clearQueue();
|
||||
res.status(205).send(); // Client should reset content.
|
||||
});
|
||||
|
||||
app.delete(api + '/queue/:index', (req, res) => {
|
||||
const index = parseInt(req.params.index, 10);
|
||||
if (isNaN(index)) {
|
||||
res.status(400).send('Invalid index');
|
||||
return;
|
||||
}
|
||||
const result: boolean | string = qm.removeFromQueue(index);
|
||||
if (result === true) {
|
||||
res.status(205).send(); // Client should reset content.
|
||||
} else {
|
||||
res.status(404).send(result); // Not found
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await qm.forceStop();
|
||||
process.exit();
|
||||
});
|
112
src/lucida.ts
112
src/lucida.ts
|
@ -1,112 +0,0 @@
|
|||
import {BrowserContext, Download, Locator, Page} from 'playwright';
|
||||
|
||||
export async function openAlbum(album: URL, context: BrowserContext): Promise<Page> {
|
||||
const page: Page = await context.newPage();
|
||||
await page.goto('/');
|
||||
|
||||
// Fill in the album URL
|
||||
await page.locator('input[id="download"]').first().fill(album.href);
|
||||
|
||||
// Wait for the XHR request to complete
|
||||
await Promise.all([
|
||||
page.waitForResponse(res => res.url().includes('/api/load') && res.status() == 200),
|
||||
page.locator('input[id="go"]').first().click(),
|
||||
page.waitForLoadState('domcontentloaded')
|
||||
]);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
export async function lucida(album: URL, baseTimeout: number, context: BrowserContext): Promise<boolean | string> {
|
||||
const page: Page = await openAlbum(album, context);
|
||||
|
||||
// Check 'Hide my download from recently downloaded' checkbox
|
||||
await page.locator('input[id="hide-from-ticker"]').first().setChecked(true);
|
||||
|
||||
// Parse info
|
||||
const albumName: string = (await page.locator('h1[class="svelte-6pt9ji"]').last().innerText()).trim();
|
||||
const trackCount: number = parseInt((await page.locator('h3[class="svelte-6pt9ji"]').first().innerText()).trim().split(' ')[0]);
|
||||
|
||||
console.log(`Downloading ${albumName} (${trackCount} tracks) from ${album.href}...`);
|
||||
console.log(`Setting timeout to ${baseTimeout * trackCount} ms...`);
|
||||
|
||||
// Start download
|
||||
await page.getByText('download full album').click();
|
||||
|
||||
let download: Download | null = null;
|
||||
const timeout: number = baseTimeout * trackCount;
|
||||
const start: number = Date.now();
|
||||
let retryCount: number = 0;
|
||||
|
||||
// Retry file download if it fails
|
||||
while (!download && (Date.now() - start) < timeout && retryCount < trackCount) {
|
||||
try {
|
||||
download = await page.waitForEvent('download');
|
||||
} catch {
|
||||
const retry: Locator = page.getByText('Retry');
|
||||
if (await retry.count() !== 0 && await retry.isVisible() && await retry.isEnabled()) {
|
||||
await page.getByText('Retry').click();
|
||||
console.log('Retrying download...');
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (download === null) {
|
||||
await page.close();
|
||||
return 'Download timed out';
|
||||
}
|
||||
|
||||
// Save the download to the Downloads folder
|
||||
// TODO Set path (configurable)
|
||||
await download.saveAs('/home/tdpeuter/Downloads/lucida/' + download.suggestedFilename());
|
||||
|
||||
// Check if the album has a booklet
|
||||
const bookPath: File | null = await booklet(album, context);
|
||||
if (bookPath !== null) {
|
||||
console.log(`Downloaded booklet ${bookPath.name}`);
|
||||
// TODO Add booklet to ZIP
|
||||
}
|
||||
|
||||
await page.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function booklet(album: URL, context: BrowserContext): Promise<File | null> {
|
||||
const page: Page = await context.newPage();
|
||||
|
||||
const bookletURL: URL = new URL('http://audiofil.hostronavt.ru/booklet.php?name=' + encodeURIComponent(album.href));
|
||||
await page.goto(bookletURL.href);
|
||||
|
||||
// Find link with goodies
|
||||
const link: Locator = page.locator('a').filter({hasText: '/goodies/'});
|
||||
const linkCount: number = await link.count();
|
||||
|
||||
if (0 <= linkCount) {
|
||||
console.log('No goodies link found');
|
||||
await page.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`Found goodies: ${await link.innerHTML()}`)
|
||||
let result: File | null = null;
|
||||
|
||||
try {
|
||||
// Go to the goodies link
|
||||
await link.dispatchEvent('click');
|
||||
|
||||
// Download the booklet
|
||||
const download: Download = await page.waitForEvent('download');
|
||||
const filename: string = download.suggestedFilename();
|
||||
await download.saveAs('/home/tdpeuter/Downloads/lucida/' + filename);
|
||||
|
||||
result = new File([await download.path()], filename);
|
||||
|
||||
console.log(`Downloaded booklet ${filename}`);
|
||||
} catch (e) {
|
||||
console.log('Could not download booklet:', e);
|
||||
}
|
||||
|
||||
await page.close();
|
||||
return result;
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
import {firefox, Browser, BrowserContext, Page} from "playwright";
|
||||
import {lucida} from "./lucida";
|
||||
|
||||
type QueueItem = {
|
||||
album: URL,
|
||||
timeout: number,
|
||||
retries?: number
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT: number = 60000;
|
||||
const MAX_RETRIES: number = 3;
|
||||
|
||||
class QueueManager {
|
||||
private browser: Browser | null;
|
||||
private queue: QueueItem[];
|
||||
|
||||
constructor(browser: Browser | null = null) {
|
||||
this.browser = browser;
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
getQueue(): QueueItem[] {
|
||||
return this.queue;
|
||||
}
|
||||
|
||||
addToQueue(link: string): boolean | string {
|
||||
// Check if the URL is a valid URL
|
||||
try {
|
||||
const url: URL = new URL(link);
|
||||
const qi: QueueItem = {
|
||||
album: url,
|
||||
timeout: DEFAULT_TIMEOUT
|
||||
};
|
||||
|
||||
// Check if the URL was already added to the queue
|
||||
if (this.queue.some(q => q.album.href === qi.album.href)) {
|
||||
return "Album already in queue";
|
||||
}
|
||||
|
||||
this.queue.push(qi);
|
||||
this.processQueue();
|
||||
return true;
|
||||
} catch {
|
||||
return "Invalid URL";
|
||||
}
|
||||
}
|
||||
|
||||
removeFromQueue(index: number): boolean | string {
|
||||
if (index < 0 || index >= this.queue.length) {
|
||||
return "Index out of bounds";
|
||||
}
|
||||
|
||||
this.queue.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
clearQueue(): void {
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
forceStop(): Promise<void> {
|
||||
if (this.browser === null) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.browser.close();
|
||||
}
|
||||
|
||||
private async processQueue(): Promise<void> {
|
||||
if (this.browser !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting browser');
|
||||
this.browser = await firefox.launch({headless: false});
|
||||
const context: BrowserContext = await this.browser.newContext({
|
||||
acceptDownloads: true,
|
||||
baseURL: 'https://lucida.to'
|
||||
});
|
||||
|
||||
while (this.queue.length > 0) {
|
||||
const qi: QueueItem | undefined = this.queue.shift();
|
||||
if (qi === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const album: URL = qi.album;
|
||||
console.log(`Processing ${album.href}`);
|
||||
const result: boolean | string = await lucida(album, qi.timeout, context);
|
||||
if (result === true) {
|
||||
console.log(`Finished processing ${album.href}`);
|
||||
} else {
|
||||
if (qi.retries === undefined) {
|
||||
qi.retries = 0;
|
||||
}
|
||||
qi.retries++;
|
||||
qi.timeout *= 2;
|
||||
if (qi.retries < MAX_RETRIES) {
|
||||
this.queue.push(qi);
|
||||
} else {
|
||||
console.error(`Failed to process ${album.href} after 3 retries: ${result}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Shutting down browser');
|
||||
await this.browser.close();
|
||||
this.browser = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default QueueManager;
|
Reference in a new issue