yt-stream
Version: 
Create easily readable streams from YouTube video url's
169 lines (159 loc) • 8.14 kB
JavaScript
const { Cookie, CookieJar, canonicalDomain } = require('tough-cookie');
const { HttpCookieAgent, HttpsCookieAgent } = require('http-cookie-agent/http');
const fs = require('fs');
const path = require('path');
function toDate(cookie){
    if(typeof cookie.expirationDate === 'string'){
        if(cookie.expirationDate.toLowerCase() === 'infinity') return 'Infinity';
        return new Date(cookie.expirationDate);
    } else if(typeof cookie.expires === 'string'){
        if(cookie.expires.toLowerCase() === 'infinity') return 'Infinity';
        return new Date(cookie.expires);
    } else if(typeof cookie.expirationDate === 'number'){
        return new Date(cookie.expirationDate * 1000);
    } else if(typeof cookie.expires === 'number'){
        return new Date(cookie.expires * 1000);
    } else return 'Infinity';
}
function addCookiesToJar(cookies, jar){
    for(const cookie of cookies){
        if(cookie instanceof Cookie){
            if(cookie.domain !== 'youtube.com' && cookie.domain !== '.youtube.com') continue;
            jar.setCookieSync(cookie, 'https://www.youtube.com');
        }
        else if(typeof cookie === 'object' && !Array.isArray(cookie) && cookie !== null){
            if(typeof cookie.key !== 'string' && typeof cookie.name !== 'string') throw new Error(`Invalid cookie. A cookie must have a key or name.`);
            if(typeof cookie.domain !== 'string') throw new Error(`Invalid cookie. A cookie must have a domain.`);
            if(cookie.domain !== 'youtube.com' && cookie.domain !== '.youtube.com') continue;
            jar.setCookieSync(new Cookie({
                key: cookie.key ?? cookie.name,
                value: typeof cookie.value === 'string' ? cookie.value : "",
                domain: canonicalDomain(cookie.domain),
                httpOnly: typeof cookie.httpOnly === 'boolean' ? cookie.httpOnly : false,
                hostOnly: typeof cookie.hostOnly === 'boolean' ? cookie.hostOnly : false,
                secure: typeof cookie.secure === 'boolean' ? cookie.secure : false,
                path: typeof cookie.path === 'string' ? cookie.path : '/',
                expires: toDate(cookie),
                sameSite: ['lax', 'samesite'].indexOf((cookie.sameSite ?? "").toLowerCase()) >= 0 ? cookie.sameSite : 'None'
            }), 'https://www.youtube.com');
        } else throw new Error(`Invalid cookie. Cookie must be an instance of Cookie class or an object.`);
    }
    return jar;
}
class YTStreamAgent {
    constructor(cookies, options){
        if(typeof options !== 'object' || Array.isArray(options) || options === null) options = {timeout: 5000, keepAlive: true, keepAliveMsecs: (Math.round(Math.random() * 3) + 5)};
        if(!(options.cookies?.jar instanceof CookieJar)){
            options.cookies = {};
            options.cookies.jar = new CookieJar();
        }
        this.jar = options.cookies.jar;
        this._options = options;
        if(typeof cookies === 'string') this.syncFile(cookies);
        else if(!Array.isArray(cookies)) cookies = [];
        if(Array.isArray(cookies)){
            if(!!!cookies.filter(c => c.name === 'SOCS').length){
                const templates = [{
                    key: 'SOCS',
                    value: 'CAI',
                    sameSite: 'lax',
                    hostOnly: false,
                    secure: true,
                    path: '/',
                    httpOnly: false,
                    domain: 'youtube.com'
                }, {
                    key: 'CONSENT',
                    value: 'PENDING+'+(Math.round(Math.random() * 400) + 1).toString(),
                    sameSite: 'None',
                    hostOnly: false,
                    secure: true,
                    path: '/',
                    httpOnly: false,
                    domain: 'youtube.com',
                    expires: 'Infinity'
                }];
                templates.forEach(t => options.cookies.jar.setCookieSync(new Cookie(t), 'https://www.youtube.com'));
            }
            options.cookies.jar = addCookiesToJar(cookies, options.cookies.jar);
        }
        this.agents = {
            https: new HttpsCookieAgent(options),
            http: new HttpCookieAgent(options)
        };
        this.localAddress = options.localAddress;
        this._cookies = this.jar.getCookiesSync('https://www.youtube.com') ?? [];
        if(typeof cookies !== 'string') this.syncedFile = '';
    }
    addCookies(cookies){
        if(!Array.isArray(cookies)) cookies = [];
        if(!(this.jar instanceof CookieJar)) throw new Error(`Jar property is not an instance of CookieJar`);
        this.jar = addCookiesToJar(cookies, this.jar);
        this._options.cookies.jar = this.jar;
        if(this.syncedFile.length > 0){
            fs.writeFileSync(this.syncedFile, JSON.stringify(this.jar.getCookiesSync('https://www.youtube.com'), null, 2));
        }
    }
    removeCookies(force){
        if(force){
            if(Object.keys(this._options).indexOf('cookies') >= 0) delete this._options.cookies;
            this.jar = new CookieJar();
            let options = {..._this._options, cookies: {jar: this.jar}};
            this.agents = {
                https: new HttpsCookieAgent(options),
                http: new HttpCookieAgent(options)
            };
        } else {
            if(!(this._options.cookies?.jar instanceof CookieJar)){
                this._options.cookies = {};
                this._options.cookies.jar = new CookieJar();
            }
    
            if(!!!this._cookies.filter(c => c.name === 'SOCS').length){
                const templates = [{
                    key: 'SOCS',
                    value: 'CAI',
                    sameSite: 'lax',
                    hostOnly: false,
                    secure: true,
                    path: '/',
                    httpOnly: false,
                    domain: 'youtube.com'
                }, {
                    key: 'CONSENT',
                    value: 'PENDING+'+(Math.round(Math.random() * 400) + 1).toString(),
                    sameSite: 'None',
                    hostOnly: false,
                    secure: true,
                    path: '/',
                    httpOnly: false,
                    domain: 'youtube.com',
                    expires: 'Infinity'
                }];
                templates.forEach(t => this._options.cookies.jar.setCookieSync(new Cookie(t), 'https://www.youtube.com'));
            }
            this._options.cookies.jar = addCookiesToJar(this._cookies, this._options.cookies.jar);
    
            this.jar = this._options.cookies.jar;
            this.agents = {
                https: new HttpsCookieAgent(this._options),
                http: new HttpCookieAgent(this._options)
            };
        }
    }
    syncFile(filePath){
        if(typeof filePath !== 'string') throw new Error(`Expected the file path to be a type of string, received ${typeof filePath}`);
        if(path.extname(filePath) !== ".json") throw new Error(`File expected to have .json extension name, received ${path.extname(filePath)}`);
        if(!path.isAbsolute(filePath)) filePath = path.join(process.cwd(), filePath);
        if(!fs.existsSync(filePath)) throw new Error(`Couldn't find a file with the path '${filePath}'. Make sure that the file exists and the path is either absolute or relative to the root of the process`);
        let cookies = fs.readFileSync(filePath);
        try{
            cookies = JSON.parse(cookies);
        } catch {
            throw new Error(`Cookies from imported file is not a valid json object`);
        }
        if(!Array.isArray(cookies)) throw new Error(`Imported cookies expected to be an array, received type of ${typeof cookies}, but no array`);
        this.syncedFile = filePath;
        this.jar = addCookiesToJar(cookies, this.jar);
        this._options.cookies.jar = this.jar;
    }
}
module.exports = { YTStreamAgent };