pdown
Version: 
Command-line tool for downloading files from Proton Drive
111 lines (110 loc) • 4.14 kB
JavaScript
import { program } from 'commander';
import packageJSON from './package.json' with { type: 'json' };
import fs from 'fs';
import path from 'path';
import puppeteer from 'puppeteer';
const selectors = {
    download: 'button[data-testid="download-button"]',
    downloadOptionDropdown: 'button[data-testid="dropdown-download-button"]',
    downloadProgress: '.transfers-manager-list-item-size',
    downloadFilename: '.transfers-manager-list-item-name'
};
program
    .name('pdown')
    .description('CLI tool for downloading files from Proton Drive')
    .usage('[options] <url>')
    .version(packageJSON.version)
    .option('-s, --speed <kbps>', 'limit download speed (in kilobytes per second)', parseInt)
    .option('-p, --path <downloadPath>', 'set download folder path', './downloads')
    .option('-u, --user-agent <userAgent>', 'override default user agent')
    .option('-c, --cookies <cookieFile>', 'path to a Netscape cookie file')
    .argument('[items...]', 'URLs/IDs of the Proton Drive files/folders to download')
    .parse(process.argv);
const options = program.opts();
const args = program.args.filter(str => str.trim().length > 0);
const urls = new Set();
args.forEach(arg => {
    switch (true) {
        case arg.startsWith('https://drive.proton.me/urls/'):
            urls.add(arg);
            break;
        case /^\w{10}#\w{12}$/.test(arg):
            urls.add(`https://drive.proton.me/urls/${arg}`);
            break;
        default:
            console.error(`Skipping invalid URL/ID: ${arg}`);
            break;
    }
});
console.log(`Downloading: ${Array.from(urls).join(', ')}`);
if (urls.size === 0) {
    console.error('At least one valid URL/ID is required.');
    program.help({ error: true });
}
if (!options.path) {
    options.path = '~/Downloads';
}
(async () => {
    const downloadPath = path.resolve(options.path);
    if (!fs.existsSync(downloadPath)) {
        fs.mkdirSync(downloadPath, { recursive: true });
    }
    const browser = await puppeteer.launch({
        headless: true,
        args: ['--no-sandbox']
    });
    const page = await browser.newPage();
    if (options.userAgent) {
        await page.setUserAgent(options.userAgent);
    }
    if (options.cookies) {
        // todo
    }
    if (options.speed) {
        const bytesPerSecond = options.speed * 1024;
        await page.emulateNetworkConditions({
            download: bytesPerSecond,
            upload: bytesPerSecond,
            latency: 10
        });
        console.log(`Throttling network to ${options.speed} KB/s`);
    }
    const client = await page.createCDPSession();
    await client.send('Page.setDownloadBehavior', { behavior: 'allow', downloadPath });
    for (const url of urls) {
        await page.goto(url, { waitUntil: 'networkidle0' });
        await page.locator(selectors['downloadOptionDropdown']).click();
        await page.locator(selectors['download']).click();
        const filename = await page.$eval(selectors['downloadFilename'], el => {
            const innerSpan = el.querySelector('span span[aria-label]');
            return innerSpan ? innerSpan.getAttribute('aria-label') : null;
        });
        let downloadComplete = false;
        while (!downloadComplete) {
            const progress = await page.$eval(selectors['downloadProgress'], el => {
                if (!el) {
                    return { percent: '', text: '' };
                }
                return {
                    percent: el.getAttribute('title')?.trim(),
                    text: el.textContent?.trim()
                };
            });
            process.stdout.write(`${filename}: ${progress.percent} ${progress.text}\r`);
            if (progress.percent === '100%') {
                downloadComplete = true;
            }
            else {
                await new Promise(resolve => setTimeout(resolve, 500));
            }
        }
    }
    ;
    for (const file of fs.readdirSync(downloadPath)) {
        if (file.endsWith('.crdownload')) {
            fs.unlinkSync(path.join(downloadPath, file));
        }
    }
    await browser.close();
})();