jd.plugins.hoster.CrunchyRollCom.java Source code

Java tutorial

Introduction

Here is the source code for jd.plugins.hoster.CrunchyRollCom.java

Source

//    jDownloader - Downloadmanager
//    Copyright (C) 2009  JD-Team support@jdownloader.org
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//    GNU General Public License for more details.
//
//    You should have received a copy of the GNU General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.

package jd.plugins.hoster;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;
import java.util.zip.InflaterInputStream;

import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import jd.PluginWrapper;
import jd.controlling.AccountController;
import jd.http.Browser;
import jd.http.URLConnectionAdapter;
import jd.nutils.encoding.Encoding;
import jd.parser.Regex;
import jd.plugins.Account;
import jd.plugins.Account.AccountType;
import jd.plugins.AccountInfo;
import jd.plugins.DownloadLink;
import jd.plugins.DownloadLink.AvailableStatus;
import jd.plugins.HostPlugin;
import jd.plugins.LinkStatus;
import jd.plugins.PluginException;
import jd.plugins.PluginForDecrypt;
import jd.utils.JDUtilities;
import jd.utils.locale.JDL;

import org.appwork.utils.formatter.TimeFormatter;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.jdownloader.downloader.hls.HLSDownloader;
import org.jdownloader.plugins.components.antiDDoSForHost;
import org.jdownloader.plugins.components.hls.HlsContainer;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

@HostPlugin(revision = "$Revision$", interfaceVersion = 3, names = { "crunchyroll.com" }, urls = {
        "http://www\\.crunchyroll\\.com/(xml/\\?req=RpcApiVideoPlayer_GetStandardConfig\\&media_id=[0-9]+.*|xml/\\?req=RpcApiSubtitle_GetXml\\&subtitle_script_id=[0-9]+.*|android_rpc/\\?req=RpcApiAndroid_GetVideoWithAcl\\&media_id=[0-9]+.*)" })
public class CrunchyRollCom extends antiDDoSForHost {

    static private Object lock = new Object();
    static private HashMap<Account, HashMap<String, String>> loginCookies = new HashMap<Account, HashMap<String, String>>();
    static private final String RCP_API_VIDEO_PLAYER = "RpcApiVideoPlayer_GetStandardConfig";
    static private final String RCP_API_SUBTITLE = "RpcApiSubtitle_GetXml";
    static private final String RCP_API_ANDROID = "RpcApiAndroid_GetVideoWithAcl";

    private String rtmp_path_or_hls_url = null;

    @SuppressWarnings("deprecation")
    public CrunchyRollCom(final PluginWrapper wrapper) {
        super(wrapper);
        this.enablePremium("http://www.crunchyroll.com/login");
    }

    /**
     * Decrypt and convert the downloaded file from CrunchyRoll's own encrypted xml format into its .ass equivalent.
     *
     * @param downloadLink
     *            The DownloadLink to convert to .ass
     */
    private void convertSubs(final DownloadLink downloadLink) throws PluginException {
        downloadLink.getLinkStatus().setStatusText("Decrypting subtitles...");
        try {

            final File source = new File(downloadLink.getFileOutput());
            final StringBuilder xmltext = new StringBuilder();
            final String lineseparator = System.getProperty("line.separator");

            Scanner in = null;
            try {
                in = new Scanner(new FileReader(source));
                while (in.hasNext()) {
                    xmltext.append(in.nextLine() + lineseparator);
                }
            } catch (Exception e) {
            } finally {
                in.close();
            }
            if (xmltext.toString().contains("<error>No Permission</error>")) {
                throw new PluginException(LinkStatus.ERROR_FILE_NOT_FOUND);
            }

            // Create the XML Parser
            final DocumentBuilderFactory xmlDocBuilderFactory = DocumentBuilderFactory.newInstance();
            final DocumentBuilder xmlDocBuilder = xmlDocBuilderFactory.newDocumentBuilder();
            final Document xml = xmlDocBuilder.parse(new File(downloadLink.getFileOutput()));
            xml.getDocumentElement().normalize();

            // Get the subtitle information
            final Element xmlSub = (Element) xml.getElementsByTagName("subtitle").item(0);

            final Node error = xmlSub.getAttributeNode("error");
            final Node xmlId = xmlSub.getAttributeNode("id");
            final Node xmlIv = xmlSub.getElementsByTagName("iv").item(0);
            final Node xmlData = xmlSub.getElementsByTagName("data").item(0);

            final int subId = Integer.parseInt(xmlId.getNodeValue());
            final String subIv = xmlIv.getTextContent();
            final String subData = xmlData.getTextContent();

            // Generate the AES parameters
            final byte[] key = this.subsGenerateKey(subId, 32);
            final byte[] ivData = DatatypeConverter.parseBase64Binary(subIv);
            final byte[] encData = DatatypeConverter.parseBase64Binary(subData);
            byte[] decrypted = null;
            try {
                final KeyParameter keyParam = new KeyParameter(key);
                final CipherParameters cipherParams = new ParametersWithIV(keyParam, ivData);

                // Prepare the cipher (AES, CBC, no padding)
                final BufferedBlockCipher cipher = new BufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
                cipher.reset();
                cipher.init(false, cipherParams);

                // Decrypt the subtitles
                decrypted = new byte[cipher.getOutputSize(encData.length)];
                final int decLength = cipher.processBytes(encData, 0, encData.length, decrypted, 0);
                cipher.doFinal(decrypted, decLength);
            } catch (final Throwable e) {
                logger.severe(e.getMessage());
                throw new PluginException(LinkStatus.ERROR_PLUGIN_DEFECT, "Error decrypting subtitles!");
            }
            // Create the XML Parser (and zlib decompress using InflaterInputStream)
            final DocumentBuilderFactory subsDocBuilderFactory = DocumentBuilderFactory.newInstance();
            final DocumentBuilder subsDocBuilder = subsDocBuilderFactory.newDocumentBuilder();
            final Document subs = subsDocBuilder
                    .parse(new InflaterInputStream(new ByteArrayInputStream(decrypted)));
            subs.getDocumentElement().normalize();

            // Get the header
            final Element subHeaderElem = (Element) subs.getElementsByTagName("subtitle_script").item(0);
            final String subHeaderTitle = subHeaderElem.getAttributeNode("title").getNodeValue();
            final String subHeaderWrap = subHeaderElem.getAttributeNode("wrap_style").getNodeValue();
            final String subHeaderResX = subHeaderElem.getAttributeNode("play_res_x").getNodeValue();
            final String subHeaderResY = subHeaderElem.getAttributeNode("play_res_y").getNodeValue();

            final String subHeader = "[Script Info]\nTitle: " + subHeaderTitle + "\nScriptType: v4.00+\nWrapStyle: "
                    + subHeaderWrap + "\nPlayResX: " + subHeaderResX + "\nPlayResY: " + subHeaderResY + "\n";

            // Get the styles
            String subStyles = "[V4 Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n";
            final NodeList subStylesNodes = subs.getElementsByTagName("style");

            for (int i = 0; i < subStylesNodes.getLength(); i++) {
                final Element subStylesElem = (Element) subStylesNodes.item(i);
                final String subStylesName = subStylesElem.getAttributeNode("name").getNodeValue();
                final String subStylesFontName = subStylesElem.getAttributeNode("font_name").getNodeValue();
                final String subStylesFontSize = subStylesElem.getAttributeNode("font_size").getNodeValue();
                final String subStylesPriColor = subStylesElem.getAttributeNode("primary_colour").getNodeValue();
                final String subStylesSecColor = subStylesElem.getAttributeNode("secondary_colour").getNodeValue();
                final String subStylesOutColor = subStylesElem.getAttributeNode("outline_colour").getNodeValue();
                final String subStylesBacColor = subStylesElem.getAttributeNode("back_colour").getNodeValue();
                final String subStylesUnderline = subStylesElem.getAttributeNode("underline").getNodeValue();
                final String subStylesStrikeout = subStylesElem.getAttributeNode("strikeout").getNodeValue();
                final String subStylesAlignment = subStylesElem.getAttributeNode("alignment").getNodeValue();
                final String subStylesSpacing = subStylesElem.getAttributeNode("spacing").getNodeValue();
                final String subStylesItalic = subStylesElem.getAttributeNode("italic").getNodeValue();
                String subStylesScaleX = subStylesElem.getAttributeNode("scale_x").getNodeValue();
                String subStylesScaleY = subStylesElem.getAttributeNode("scale_y").getNodeValue();
                final String subStylesBorder = subStylesElem.getAttributeNode("border_style").getNodeValue();
                final String subStylesShadow = subStylesElem.getAttributeNode("shadow").getNodeValue();
                final String subStylesBold = subStylesElem.getAttributeNode("bold").getNodeValue();
                final String subStylesAngle = subStylesElem.getAttributeNode("angle").getNodeValue();
                final String subStylesOutline = subStylesElem.getAttributeNode("outline").getNodeValue();
                final String subStylesMarginL = subStylesElem.getAttributeNode("margin_l").getNodeValue();
                final String subStylesMarginR = subStylesElem.getAttributeNode("margin_r").getNodeValue();
                final String subStylesMarginV = subStylesElem.getAttributeNode("margin_v").getNodeValue();
                final String subStylesEncoding = subStylesElem.getAttributeNode("encoding").getNodeValue();

                // Fix the odd case where the subtitles are scaled to nothing
                if (subStylesScaleX.equals("0")) {
                    subStylesScaleX = "100";
                }
                if (subStylesScaleY.equals("0")) {
                    subStylesScaleY = "100";
                }

                subStyles += "Style: " + subStylesName + ", " + subStylesFontName + ", " + subStylesFontSize + ", "
                        + subStylesPriColor + ", " + subStylesSecColor + ", " + subStylesOutColor + ", "
                        + subStylesBacColor + ", " + subStylesBold + ", " + subStylesItalic + ", "
                        + subStylesUnderline + ", " + subStylesStrikeout + ", " + subStylesScaleX + ", "
                        + subStylesScaleY + ", " + subStylesSpacing + ", " + subStylesAngle + ", " + subStylesBorder
                        + ", " + subStylesOutline + ", " + subStylesShadow + ", " + subStylesAlignment + ", "
                        + subStylesMarginL + ", " + subStylesMarginR + ", " + subStylesMarginV + ", "
                        + subStylesEncoding + "\n";
            }

            // Get the elements
            String subEvents = "[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n";
            final NodeList subEventsNodes = subs.getElementsByTagName("event");

            for (int i = 0; i < subEventsNodes.getLength(); i++) {
                final Element subEventsElem = (Element) subEventsNodes.item(i);
                final String subEventsStart = subEventsElem.getAttributeNode("start").getNodeValue();
                final String subEventsEnd = subEventsElem.getAttributeNode("end").getNodeValue();
                final String subEventsStyle = subEventsElem.getAttributeNode("style").getNodeValue();
                final String subEventsName = subEventsElem.getAttributeNode("name").getNodeValue();
                final String subEventsMarginL = subEventsElem.getAttributeNode("margin_l").getNodeValue();
                final String subEventsMarginR = subEventsElem.getAttributeNode("margin_r").getNodeValue();
                final String subEventsMarginV = subEventsElem.getAttributeNode("margin_v").getNodeValue();
                final String subEventsEffect = subEventsElem.getAttributeNode("effect").getNodeValue();
                final String subEventsText = subEventsElem.getAttributeNode("text").getNodeValue();

                subEvents += "Dialogue: 0," + subEventsStart + "," + subEventsEnd + "," + subEventsStyle + ","
                        + subEventsName + "," + subEventsMarginL + "," + subEventsMarginR + "," + subEventsMarginV
                        + "," + subEventsEffect + "," + subEventsText + "\n";
            }

            // Output to the original file
            final FileWriter subOutFile = new FileWriter(downloadLink.getFileOutput());
            final BufferedWriter subOut = new BufferedWriter(subOutFile);

            try {
                subOut.write(subHeader + "\n");
                subOut.write(subStyles + "\n");
                subOut.write(subEvents);
            } catch (final Throwable e) {
                subOut.close();
                subOutFile.close();
                throw new PluginException(LinkStatus.ERROR_DOWNLOAD_FAILED, "Error writing decrypted subtitles!");
            }

            subOut.close();
            subOutFile.close();
            downloadLink.getLinkStatus().setStatusText(
                    JDL.L("plugins.hoster.crunchyrollcom.decryptedsubtitles", "Subtitles decrypted"));
        } catch (final SAXException e) {
            throw new PluginException(LinkStatus.ERROR_PLUGIN_DEFECT,
                    "Error decrypting subtitles: Invalid XML file!");
        } catch (final DOMException e) {
            throw new PluginException(LinkStatus.ERROR_PLUGIN_DEFECT,
                    "Error decrypting subtitles: XML file changed!");
        } catch (final PluginException e) {
            throw e;
        } catch (final Throwable e) {
            e.printStackTrace();
            throw new PluginException(LinkStatus.ERROR_PLUGIN_DEFECT, "Error decrypting subtitles!");
        }
    }

    /**
     * Download the given file using the HTTP Android method. The file will be mp4 and have subtitles hardcoded.
     *
     * @param downloadLink
     *            The DownloadLink to try and download using RTMP
     */
    private void downloadAndroid(final DownloadLink downloadLink) throws Exception {
        // Check if the link appears to be valid
        final String videoUrl = downloadLink.getStringProperty("videourl");
        if ((Boolean) downloadLink.getProperty("valid", false) && videoUrl != null) {
            this.dl = jd.plugins.BrowserAdapter.openDownload(this.br, downloadLink, videoUrl, true, 0);
            if (!this.dl.getConnection().isContentDisposition()
                    && !this.dl.getConnection().getContentType().startsWith("video")) {
                downloadLink.setProperty("valid", false);
                this.dl.getConnection().disconnect();
                throw new PluginException(LinkStatus.ERROR_RETRY);
            }
            this.dl.startDownload();
        }
    }

    /**
     * Attempt to download the given file using RTMP (rtmpdump). Needs to use the properties "valid", "rtmphost", "rtmpfile", "rtmpswf",
     * "swfdir". These are set by jd.plugins.decrypter.CrchyRollCom.setRMP() through requestFileInformation()
     *
     * @param downloadLink
     *            The DownloadLink to try and download using RTMP
     */
    private void downloadRTMP(final DownloadLink downloadLink) throws Exception {
        // Check if the link appears to be valid
        if ((Boolean) downloadLink.getProperty("valid", false)
                && downloadLink.getStringProperty("rtmphost").startsWith("rtmp")) {
            final String url = downloadLink.getStringProperty("rtmphost") + "/"
                    + downloadLink.getStringProperty("rtmpfile");

            // Create the download
            this.dl = new RTMPDownload(this, downloadLink, url);
            final jd.network.rtmp.url.RtmpUrlConnection rtmp = ((RTMPDownload) this.dl).getRtmpConnection();

            // Set all of the needed rtmpdump parameters
            rtmp.setUrl(url);
            rtmp.setTcUrl(downloadLink.getStringProperty("rtmphost"));
            rtmp.setPlayPath(rtmp_path_or_hls_url);
            rtmp.setSwfVfy(downloadLink.getStringProperty("swfdir") + downloadLink.getStringProperty("rtmpswf"));
            rtmp.setResume(true);

            ((RTMPDownload) this.dl).startDownload();
        } else {
            throw new PluginException(LinkStatus.ERROR_DOWNLOAD_FAILED, "Invalid download");
        }
    }

    /**
     * Download subtitles and convert them to .ass
     *
     * @param downloadLink
     *            The DownloadLink to try and download convert to .ass
     */
    private void downloadSubs(final DownloadLink downloadLink) throws Exception {
        if ((Boolean) downloadLink.getProperty("valid", false)) {
            this.dl = jd.plugins.BrowserAdapter.openDownload(this.br, downloadLink, downloadLink.getDownloadURL(),
                    true, 1);
            if (!this.dl.getConnection().isContentDisposition()
                    && !this.dl.getConnection().getContentType().endsWith("xml")) {
                downloadLink.setProperty("valid", false);
                this.dl.getConnection().disconnect();
                throw new PluginException(LinkStatus.ERROR_RETRY);
            }
            if (this.dl.startDownload()) {
                this.convertSubs(downloadLink);
            }
        }
    }

    private void downloadHls(final DownloadLink downloadLink) throws Exception {
        getPage(rtmp_path_or_hls_url);
        final HlsContainer hlsbest = HlsContainer.findBestVideoByBandwidth(HlsContainer.getHlsQualities(this.br));
        if (hlsbest == null) {
            throw new PluginException(LinkStatus.ERROR_PLUGIN_DEFECT);
        }
        if (!downloadLink.getFinalFileName().endsWith(".mp4")) {
            downloadLink.setFinalFileName(downloadLink.getFinalFileName() + ".mp4");
        }
        rtmp_path_or_hls_url = hlsbest.getDownloadurl();
        checkFFmpeg(downloadLink, "Download a HLS Stream");
        /* 2016-10-19: Seems like they use crypted HLS (in most cases?!) */
        dl = new HLSDownloader(downloadLink, br, rtmp_path_or_hls_url);
        dl.startDownload();
    }

    @Override
    public AccountInfo fetchAccountInfo(final Account account) throws Exception {
        final AccountInfo ai = new AccountInfo();
        try {
            this.login(account, this.br, true);
            // TODO Find the expiration date of the premium status
        } catch (final PluginException e) {
            account.setValid(false);
            return ai;
        }
        // date + 4 days
        final String nextbillingdate = br
                .getRegex("Next Billing Date:</th>\\s*<td>([a-zA-Z]{3,4} \\d{1,2}, \\d{4})</td>").getMatch(0);
        long date = TimeFormatter.getMilliSeconds(nextbillingdate, "MMM dd, yyyy", Locale.ENGLISH);
        if (date > 0) {
            date += (4 * 24 * 60 * 60 * 1000l);
        }
        ai.setValidUntil(date, br);
        if (!ai.isExpired()) {
            account.setType(AccountType.PREMIUM);
            ai.setStatus("Premium Account");
        } else {
            account.setType(AccountType.FREE);
            ai.setStatus("Free Account");
            ai.setValidUntil(-1);
        }
        account.setValid(true);
        return ai;
    }

    @Override
    public String getAGBLink() {
        return "http://www.crunchyroll.com/tos";
    }

    @Override
    public String getDescription() {
        return "JDownloader's CrunchyRoll Plugin helps download videos and subtitles from crunchyroll.com. Crunchyroll provides a range of qualities, and this plugin will show all those available to you.";
    }

    @Override
    public int getMaxSimultanFreeDownloadNum() {
        return -1;
    }

    @Override
    public int getMaxSimultanPremiumDownloadNum() {
        return -1;
    }

    @Override
    public void handleFree(final DownloadLink downloadLink) throws Exception {
        downloadLink.setProperty("valid", false);
        this.requestFileInformation(downloadLink);
        if (rtmp_path_or_hls_url != null && rtmp_path_or_hls_url.contains(".m3u8")) {
            downloadHls(downloadLink);
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_VIDEO_PLAYER)) {
            this.downloadRTMP(downloadLink);
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_SUBTITLE)) {
            this.downloadSubs(downloadLink);
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_ANDROID)) {
            this.downloadAndroid(downloadLink);
        }
    }

    @Override
    public void handlePremium(final DownloadLink downloadLink, final Account account) throws Exception {
        downloadLink.setProperty("valid", false);
        this.login(account, this.br, false);
        this.requestFileInformation(downloadLink);
        if (rtmp_path_or_hls_url != null && rtmp_path_or_hls_url.contains(".m3u8")) {
            downloadHls(downloadLink);
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_VIDEO_PLAYER)) {
            this.downloadRTMP(downloadLink);
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_SUBTITLE)) {
            this.downloadSubs(downloadLink);
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_ANDROID)) {
            this.downloadAndroid(downloadLink);
        }
    }

    /**
     * Attempt to log into crunchyroll.com using the given account. Cookies are cached to 'loginCookies'.
     *
     * @param account
     *            The account to use to log in.
     * @param br
     *            The browser to use to log in. This is the browser where the cookies will be saved.
     * @param refresh
     *            Should new cookies be retrieved (fresh login) even if cookies have previously been cached.
     */
    public void login(final Account account, Browser br, final boolean refresh) throws Exception {
        synchronized (CrunchyRollCom.lock) {
            if (br == null) {
                br = this.br;
            }
            try {
                this.setBrowserExclusive();
                // Load cookies from the cache if allowed, and they exist
                if (refresh == false && CrunchyRollCom.loginCookies.containsKey(account)) {
                    final HashMap<String, String> cookies = CrunchyRollCom.loginCookies.get(account);
                    if (cookies != null) {
                        if (cookies.containsKey("c_userid")) {
                            // Save cookies to the browser
                            for (final Map.Entry<String, String> cookieEntry : cookies.entrySet()) {
                                final String key = cookieEntry.getKey();
                                final String value = cookieEntry.getValue();
                                br.setCookie("crunchyroll.com", key, value);
                            }
                            return;
                        }
                    }
                }

                // Set the POST parameters to log in
                final LinkedHashMap<String, String> post = new LinkedHashMap<String, String>();
                post.put("formname", "RpcApiUser_Login");
                post.put("next_url", Encoding.urlEncode("http://www.crunchyroll.com/acct/membership/"));
                post.put("fail_url", Encoding.urlEncode("http://www.crunchyroll.com/login"));
                post.put("name", Encoding.urlEncode(account.getUser()));
                post.put("password", Encoding.urlEncode(account.getPass()));
                post.put("submit", "submit");

                // Load the login page (actually log in)
                br.setFollowRedirects(true);
                postPage("https://www.crunchyroll.com/?a=formhandler", post);
                // redirect => via standard means, then another direct => via Meta/JavaScript only
                String redirect = br.getRegex("<meta http-equiv=\"refresh\" content=\"\\d+;\\s*url=(.*?)\"")
                        .getMatch(0);
                if (redirect == null) {
                    redirect = br.getRegex("document.location.href=\"(.*?)\"").getMatch(0);
                    if (redirect != null) {
                        redirect = redirect.replace("\\/", "/");
                    }
                }
                if (redirect != null) {
                    getPage(br, redirect);
                }
                if (br.getCookie(this.getHost(), "c_userid") == null
                        && br.getCookie(this.getHost(), "c_userkey") == null) {
                    // Set account to invalid and quit
                    account.setValid(false);
                    if ("de".equalsIgnoreCase(System.getProperty("user.language"))) {
                        throw new PluginException(LinkStatus.ERROR_PREMIUM,
                                "\r\nUngltiger Benutzername oder ungltiges Passwort!\r\nDu bist dir sicher, dass dein eingegebener Benutzername und Passwort stimmen? Versuche folgendes:\r\n1. Falls dein Passwort Sonderzeichen enthlt, ndere es (entferne diese) und versuche es erneut!\r\n2. Gib deine Zugangsdaten per Hand (ohne kopieren/einfgen) ein.",
                                PluginException.VALUE_ID_PREMIUM_DISABLE);
                    } else {
                        throw new PluginException(LinkStatus.ERROR_PREMIUM,
                                "\r\nInvalid username/password!\r\nYou're sure that the username and password you entered are correct? Some hints:\r\n1. If your password contains special characters, change it (remove them) and try again!\r\n2. Type in your username/password by hand without copy & paste.",
                                PluginException.VALUE_ID_PREMIUM_DISABLE);
                    }
                }

                // Save the cookies to the cache
                CrunchyRollCom.loginCookies.put(account, fetchCookies("crunchyroll.com"));
            } catch (final PluginException e) {
                CrunchyRollCom.loginCookies.remove(account);
                throw e;
            }
        }
    }

    /**
     * Pad and format version numbers so that the String.compare() method can be used simply. ("9.10.2", ".", 4) would result in
     * "000900100002".
     *
     * @param version
     *            The version number string to format (e.g. '9.10.2')
     * @param sep
     *            The character(s) to split the numbers by (e.g. '.')
     * @param maxWidth
     *            The number of digits to pad the numbers to (e.g. 5 would make '12' become '00012'). Note that numbers which exceed this
     *            are not truncated.
     * @return The formatted version number ready to be compared
     */
    private String normaliseRtmpVersion(final String version, final String sep, final int maxWidth) {
        final String[] split = Pattern.compile(sep, Pattern.LITERAL).split(version);
        final StringBuilder sb = new StringBuilder();
        for (final String s : split) {
            sb.append(String.format("%" + maxWidth + 's', s).replace(' ', '0'));
        }
        return sb.toString();
    }

    /**
     * JD2 CODE. DO NOT USE OVERRIDE FOR JD=) COMPATIBILITY REASONS!
     */
    public boolean isProxyRotationEnabledForLinkChecker() {
        return false;
    }

    @SuppressWarnings("deprecation")
    @Override
    public AvailableStatus requestFileInformation(final DownloadLink downloadLink) throws Exception {
        downloadLink.setProperty("valid", false);
        rtmp_path_or_hls_url = getRtmpPathOrHlsUrl(downloadLink);

        // Try and find which download type it is
        if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_VIDEO_PLAYER)) {
            // Attempt to login
            if (this.br.getCookies("crunchyroll.com").isEmpty()) {
                final Account account = AccountController.getInstance().getValidAccount(this);
                if (account != null) {
                    try {
                        this.login(account, this.br, false);
                    } catch (final Exception e) {
                    }
                }
            }

            // Find matching decrypter
            final PluginForDecrypt plugin = JDUtilities.getPluginForDecrypt("crunchyroll.com");
            if (plugin == null) {
                throw new PluginException(LinkStatus.ERROR_PLUGIN_DEFECT, "Cannot decrypt video link");
            }

            // Set the RTMP details (exception on error)
            try {
                ((jd.plugins.decrypter.CrhyRllCom) plugin).setRTMP(downloadLink, this.br);
            } catch (final PluginException e) {
                throw new PluginException(LinkStatus.ERROR_FILE_NOT_FOUND);
            }
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_SUBTITLE)) {
            // Validate the URL and set filename
            final String subId = new Regex(downloadLink.getDownloadURL(), "subtitle_script_id=([0-9]+)")
                    .getMatch(0);
            if (subId == null) {
                return AvailableStatus.FALSE;
            }

            if (downloadLink.getStringProperty("filename") == null) {
                final String filename = "CrunchyRoll." + subId;
                downloadLink.setProperty("filename", filename);
                downloadLink.setFinalFileName(filename + ".ass");
            } else if (downloadLink.getFinalFileName() == null) {
                downloadLink.setFinalFileName(downloadLink.getStringProperty("filename") + ".ass");
            }
            /* Get xml page here to find possible errors */
            getPage(downloadLink.getDownloadURL());
            if (br.containsHTML("<error>No Permission</error>")) {
                throw new PluginException(LinkStatus.ERROR_FILE_NOT_FOUND);
            }

            // Get the HTTP response headers of the XML file to check for validity
            URLConnectionAdapter conn = null;
            try {
                conn = this.br.openGetConnection(downloadLink.getDownloadURL());
                final long respCode = conn.getResponseCode();
                final long length = conn.getLongContentLength();
                final String contType = conn.getContentType();
                if (respCode == 200 && contType.endsWith("xml")) {
                    // Check if the file is too small to be subtitles
                    // 20130719 length isn't given anymore so will equal -1
                    // 20141123 length is always -1, code below is not usable, getpage and error check is added above
                    if (length != -1 && length < 200) {
                        return AvailableStatus.FALSE;
                    }

                    // File valid, set details
                    downloadLink.setDownloadSize(length);
                    downloadLink.setProperty("valid", true);
                }
            } finally {
                try {
                    conn.disconnect();
                } catch (final Throwable e) {
                }
            }
        } else if (downloadLink.getDownloadURL().contains(CrunchyRollCom.RCP_API_ANDROID)) {
            // Find matching decrypter
            final PluginForDecrypt plugin = JDUtilities.getPluginForDecrypt("crunchyroll.com");
            if (plugin == null) {
                throw new PluginException(LinkStatus.ERROR_PLUGIN_DEFECT, "Cannot decrypt video link");
            }

            // Set the Android video details (exception on error)
            ((jd.plugins.decrypter.CrhyRllCom) plugin).setAndroid(downloadLink, this.br);
        }
        if ((Boolean) downloadLink.getProperty("valid", false)) {
            return AvailableStatus.TRUE;
        }
        return AvailableStatus.FALSE;
    }

    private String getRtmpPathOrHlsUrl(final DownloadLink dl) {
        return dl.getStringProperty("rtmpfile", null);
    }

    @Override
    public void reset() {
    }

    @Override
    public void resetDownloadlink(final DownloadLink link) {
    }

    @Override
    public void resetPluginGlobals() {
    }

    /**
     * Generate the AES decryption key based on the subtitle's id using some obfuscation and SHA-1 hashing.
     *
     * @param id
     *            The id of the subtitles to generate the key for
     * @param size
     *            The number of bytes to make the key (e.g. 32 bytes for 256-bit key)
     * @return The byte formatted key to be used in AES decryption
     */
    private byte[] subsGenerateKey(final int id, final int size) throws NoSuchAlgorithmException {
        // Generate fibonacci salt
        String magicStr = "";
        int fibA = 1;
        int fibB = 2;
        for (int i = 2; i < 22; i++) {
            final int newChr = fibA + fibB;
            fibA = fibB;
            fibB = newChr;
            magicStr += Character.toString((char) (newChr % 97 + 33));
        }

        // Calculate magic number
        final int magic1 = (int) Math.floor(Math.sqrt(6.9) * Math.pow(2, 25));
        final long magic2 = id ^ magic1 ^ (id ^ magic1) >>> 3 ^ (magic1 ^ id) * 32l;

        magicStr += magic2;

        // Calculate the hash using SHA-1
        final MessageDigest md = MessageDigest.getInstance("SHA-1");
        /* CHECK: we should always use getBytes("UTF-8") or with wanted charset, never system charset! */
        final byte[] magicBytes = magicStr.getBytes();
        md.update(magicBytes, 0, magicBytes.length);
        final byte[] hashBytes = md.digest();

        // Create the key using the given length
        final byte[] key = new byte[size];
        Arrays.fill(key, (byte) 0);

        for (int i = 0; i < key.length && i < hashBytes.length; i++) {
            key[i] = hashBytes[i];
        }
        return key;
    }

    /**
     * because stable is lame!
     */
    public void setBrowser(final Browser ibr) {
        this.br = ibr;
    }

    // required for decrypter
    public void getPage(final Browser ibr, final String page) throws Exception {
        super.getPage(ibr, page);
    }

    // required for decrypter
    public void postPage(final Browser ibr, final String page, final String postData) throws Exception {
        super.postPage(ibr, page, postData);
    }

}