<commit_before><link rel="import" href="../connector.html">

<script>

    /**
     *
     */
    class CrunchyManga extends Connector {

        /**
         *
         */
        constructor() {
            super();
            // Public members for usage in UI (mandatory)
            super.id         = 'crunchymanga';
            super.label      = 'CrunchyManga (Premium)';
            super.isLocked   = false;
            // Private members for internal usage only (convenience)
            this.url         = 'http://api-manga.crunchyroll.com';
            this.reqOptionsJSON = JSON.stringify( {
                url: this.url,
                json: {} // => use to automatically parse a response as json
            } );
            this.initialized = false;
            this.premium     = false;
            this.session     = null;
            this.token       = null;
            // Private members for internal use that can be configured by the user through settings menu (set to undefined or false to hide from settings menu!)
            this.config = {
                username: {
                    label: 'Username',
                    description: 'Username for login with CrunchyRoll Premium account (leave blank for guest account with limited access).\nDue to HakuNeko\'s low security level you should not use your real account!',
                    input: Input.text,
                    value: ''
                },
                password: {
                    label: 'Password',
                    description: 'Password for login with CrunchyRoll Premium account (leave blank for guest account with limited access).\nDue to HakuNeko\'s low security level you should not use your real account!',
                    input: Input.password,
                    value: ''
                }
            };
            document.addEventListener( EventListener.onSettingsChanged, this._onSettingsChanged.bind( this ) );

            // register a protocol handler to intercept requests (with custom 'crunchymanga' protocol) and decrypt images
            Engine.Request.registerProtocol( this.id, ( request, callback ) => {
                let uri = new URL( request.url );
                uri.protocol = ( new URL( this.url ) ).protocol;
                let opts = {
                    url: uri.href,
                    encoding: null
                }
                Engine.Request.fetch( opts, ( error, response, content ) => {
                    callback( {
                        mimeType: response.headers['content-type'],
                        data: this._decryptImage( content, 0x42 )
                    } );
                });
            } );
        }

        /**
         *
         */
        _getRequestError( error, response, content ) {
            if( error ) {
                return error;
            }
            if( response.statusCode !== 200 ) {
                return new Error( `Response status code: ${response.statusCode}!` );
            }
            try {
                if( content.__class__ === 'error' ) {
                    return new Error( content.type + ': ' + content.message )
                }
            } catch ( e ) {
                return new Error( '[' + response.statusCode + '] ' + e.message );
            }
            return null;
        }

        /**
         *
         */
        _getMangaList( callback ) {
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] += '/series';
            options['qs'] = {
                'session_id': this.session,
                'auth': this.token
            };
            Engine.Request.fetch( options, ( error, response, content ) => {
                error = this._getRequestError( error, response, content);
                callback( error, ( !error ? content.map( ( manga ) => {
                    return {
                        id: manga.series_id,
                        title: ( manga.locale && manga.locale.enUS ? manga.locale.enUS.name : manga.url.replace( /^\// , '' ) )
                    }
                } ) : [] ) );
            });
        }

        /**
         *
         */
        _getChapterList( manga, callback ) {
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] += '/chapters';
            options['qs'] = {
                'series_id': manga.id,
                'session_id': this.session,
                'auth': this.token
            };
            Engine.Request.fetch( options, ( error, response, content ) => {
                error = this._getRequestError( error, response, content );
                callback( error, ( !error ? content.chapters.reverse().map( ( chapter ) => {
                    // chapter.volume_number
                    return {
                        id: chapter.chapter_id,
                        title: ( '0000' + chapter.number ).slice( -7 ) + ( chapter.locale && chapter.locale.enUS ? ' - ' + chapter.locale.enUS.name : '' ),
                        language: 'en'
                    }
                }) : [] ) );
            });
        }

        /**
         *
         */
        _getPageList( manga, chapter, callback ) {
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] += '/list_chapter';
            options['qs'] = {
                'chapter_id': chapter.id,
                'session_id': this.session,
                'auth': this.token
            };
            Engine.Request.fetch( options, ( error, response, content ) => {
                error = this._getRequestError( error, response, content );
                if( error ) {
                    callback( error, [] );
                    return;
                }
                let pages = content.pages.map( ( page ) => {
                    //let uri = new URL( page['image_url'] ); // sometimes invalid
                    //let uri = new URL( page.locale.enUS['image_url'] ); // this seems to be the raw image (png), but access is forbidden
                    //let uri = new URL( page.locale.enUS['composed_image_url'] ); // access is forbidden
                    //let uri = new URL( page.locale.enUS['encrypted_mobile_image_url'] ); // smaller size
                    let uri = new URL( page.locale.enUS['encrypted_composed_image_url'] );
                    // use dedicated protocol handler for image decryption from crunchyroll
                    uri.protocol = this.id;
                    return uri.href;
                });
                callback( error, ( !error && content ? pages : [] ) );
            });
        }

        /**
        *
        */
        _onSettingsChanged( e ) {
            this._login( this.config.username.value, this.config.password.value );
        }

        /**
         * Login to crunchyroll website with username and password from settings to
         * get full access to all chapters.
         */
         _login( username, password ) {
            if( !username || username.length < 1 || !password || password.length < 1 ) {
                this.initialized = undefined;
                return;
            }
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] = 'https://www.crunchyroll.com/login';
            options['json'] = undefined;
            Engine.Request.cloudflare( options, ( error, response, content ) => {
                // TODO: error handling
                content = content.replace( /<img[^<]*?>/g, '<img>');
                content = content.replace( /<iframe[^<]*?>/g, '<iframe>');
                let dom = document.createElement( 'html' );
                dom.innerHTML = content;
                let token = dom.querySelector( 'input#login_form__token' ).value;
                options['method'] = 'POST';
                options['form'] = {
                    'login_form[name]': username,
                    'login_form[password]': password,
                    'login_form[redirect_url]': '',
                    'login_form[_token]': token
                };
                Engine.Request.cloudflare( options, ( error, response, content ) => {
                    // TODO: error handling
                    this.session = response.headers['set-cookie'].reverse().find( ( cookie ) => {
                        return ( cookie.indexOf( 'sess_id=' ) === 0 && cookie.indexOf( 'sess_id=deleted' ) < 0 );
                    }).split('=')[1].split(';')[0];
                    options['url'] = `${this.url}/cr_authenticate`;
                    options['method'] = 'GET';<commit_msg>[crunchyroll] added provisional login erro handling<commit_after><link rel="import" href="../connector.html">

<script>

    /**
     *
     */
    class CrunchyManga extends Connector {

        /**
         *
         */
        constructor() {
            super();
            // Public members for usage in UI (mandatory)
            super.id         = 'crunchymanga';
            super.label      = 'CrunchyManga (Premium)';
            super.isLocked   = false;
            // Private members for internal usage only (convenience)
            this.url         = 'http://api-manga.crunchyroll.com';
            this.reqOptionsJSON = JSON.stringify( {
                url: this.url,
                json: {} // => use to automatically parse a response as json
            } );
            this.initialized = false;
            this.premium     = false;
            this.session     = null;
            this.token       = null;
            // Private members for internal use that can be configured by the user through settings menu (set to undefined or false to hide from settings menu!)
            this.config = {
                username: {
                    label: 'Username',
                    description: 'Username for login with CrunchyRoll Premium account (leave blank for guest account with limited access).\nDue to HakuNeko\'s low security level you should not use your real account!',
                    input: Input.text,
                    value: ''
                },
                password: {
                    label: 'Password',
                    description: 'Password for login with CrunchyRoll Premium account (leave blank for guest account with limited access).\nDue to HakuNeko\'s low security level you should not use your real account!',
                    input: Input.password,
                    value: ''
                }
            };
            document.addEventListener( EventListener.onSettingsChanged, this._onSettingsChanged.bind( this ) );

            // register a protocol handler to intercept requests (with custom 'crunchymanga' protocol) and decrypt images
            Engine.Request.registerProtocol( this.id, ( request, callback ) => {
                let uri = new URL( request.url );
                uri.protocol = ( new URL( this.url ) ).protocol;
                let opts = {
                    url: uri.href,
                    encoding: null
                }
                Engine.Request.fetch( opts, ( error, response, content ) => {
                    callback( {
                        mimeType: response.headers['content-type'],
                        data: this._decryptImage( content, 0x42 )
                    } );
                });
            } );
        }

        /**
         *
         */
        _getRequestError( error, response, content ) {
            if( error ) {
                return error;
            }
            if( response.statusCode !== 200 ) {
                return new Error( `Response status code: ${response.statusCode}!` );
            }
            try {
                if( content.__class__ === 'error' ) {
                    return new Error( content.type + ': ' + content.message )
                }
            } catch ( e ) {
                return new Error( '[' + response.statusCode + '] ' + e.message );
            }
            return null;
        }

        /**
         *
         */
        _getMangaList( callback ) {
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] += '/series';
            options['qs'] = {
                'session_id': this.session,
                'auth': this.token
            };
            Engine.Request.fetch( options, ( error, response, content ) => {
                error = this._getRequestError( error, response, content);
                callback( error, ( !error ? content.map( ( manga ) => {
                    return {
                        id: manga.series_id,
                        title: ( manga.locale && manga.locale.enUS ? manga.locale.enUS.name : manga.url.replace( /^\// , '' ) )
                    }
                } ) : [] ) );
            });
        }

        /**
         *
         */
        _getChapterList( manga, callback ) {
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] += '/chapters';
            options['qs'] = {
                'series_id': manga.id,
                'session_id': this.session,
                'auth': this.token
            };
            Engine.Request.fetch( options, ( error, response, content ) => {
                error = this._getRequestError( error, response, content );
                callback( error, ( !error ? content.chapters.reverse().map( ( chapter ) => {
                    // chapter.volume_number
                    return {
                        id: chapter.chapter_id,
                        title: ( '0000' + chapter.number ).slice( -7 ) + ( chapter.locale && chapter.locale.enUS ? ' - ' + chapter.locale.enUS.name : '' ),
                        language: 'en'
                    }
                }) : [] ) );
            });
        }

        /**
         *
         */
        _getPageList( manga, chapter, callback ) {
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] += '/list_chapter';
            options['qs'] = {
                'chapter_id': chapter.id,
                'session_id': this.session,
                'auth': this.token
            };
            Engine.Request.fetch( options, ( error, response, content ) => {
                error = this._getRequestError( error, response, content );
                if( error ) {
                    callback( error, [] );
                    return;
                }
                let pages = content.pages.map( ( page ) => {
                    //let uri = new URL( page['image_url'] ); // sometimes invalid
                    //let uri = new URL( page.locale.enUS['image_url'] ); // this seems to be the raw image (png), but access is forbidden
                    //let uri = new URL( page.locale.enUS['composed_image_url'] ); // access is forbidden
                    //let uri = new URL( page.locale.enUS['encrypted_mobile_image_url'] ); // smaller size
                    let uri = new URL( page.locale.enUS['encrypted_composed_image_url'] );
                    // use dedicated protocol handler for image decryption from crunchyroll
                    uri.protocol = this.id;
                    return uri.href;
                });
                callback( error, ( !error && content ? pages : [] ) );
            });
        }

        /**
        *
        */
        _onSettingsChanged( e ) {
            this._login( this.config.username.value, this.config.password.value );
        }

        /**
         * Login to crunchyroll website with username and password from settings to
         * get full access to all chapters.
         */
         _login( username, password ) {
            if( !username || username.length < 1 || !password || password.length < 1 ) {
                this.initialized = undefined;
                return;
            }
            let options = JSON.parse( this.reqOptionsJSON );
            options['url'] = 'https://www.crunchyroll.com/login';
            options['json'] = undefined;
            Engine.Request.cloudflare( options, ( error, response, content ) => {
                // TODO: error handling
                if( error ) {
                    this.initialized = true;
                    console.error( error );
                    return;
                }
                content = content.replace( /<img[^<]*?>/g, '<img>');
                content = content.replace( /<iframe[^<]*?>/g, '<iframe>');
                let dom = document.createElement( 'html' );
                dom.innerHTML = content;
                let token = dom.querySelector( 'input#login_form__token' ).value;
                options['method'] = 'POST';
                options['form'] = {
                    'login_form[name]': username,
                    'login_form[password]': password,
                    'login_form[redirect_url]': '',
                    'login_form[_token]': token
                };
                Engine.Request.cloudflare( options, ( error, response, content ) => {
                    // TODO: error handling
                    if( error ) {
                        this.initialized = true;
                        console.error( error );
                        return;
                    }
                    this.session = response.headers['set-cookie'].reverse().find( ( cookie ) => {
                        return ( cookie.indexOf( 'sess_id=' ) === 0 && cookie.indexOf( 'sess_id=deleted' ) < 0 );
                    }).split('=')[1].split(';')[0];
                    options['url'] = `${this.url}/cr_authenticate`;
                    options['method'] = 'GET';
{'fraction_non_alphanumeric': 0.11944033670799681, 'fraction_numerical': 0.0019337959276532818, 'mean_word_length': 1.278180875874579, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 39, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1215023', 'n_tokens_mistral': 4452, 'n_tokens_neox': 4152, 'n_words': 1050}