Seriesfeed++

A fork of Bierdopje AddOn Plus for Seriesfeed

当前为 2015-12-12 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Seriesfeed++
// @namespace    https://greasyfork.org/en/users/22592
// @description  A fork of Bierdopje AddOn Plus for Seriesfeed
// @include      http://www.seriesfeed.com/*
// @version      1.04
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @require      http://code.jquery.com/jquery-1.10.2.js
// @require      http://code.jquery.com/ui/1.11.4/jquery-ui.js
// @author       Mr. Invisible
// @run-at       document-end
// ==/UserScript==

/*global GM_getValue,GM_registerMenuCommand,GM_addStyle,GM_info,GM_setValue,$ */

/**
 Changelog:
 1.04: Fixed issue with the visual watchlist & dialog for download now closes after clicking a link.
 1.03: Updated for SeriesFeed 1.2
 1.02: Fixed small bug with Chrome-derived browsers
 1.01: Rewrote script in order to accommodate the SeriesFeed pages
 1.00: Cloned from the Bierdopje AddOn Plus version 1.101
 **/

// Create one accessible object. The remainder is hidden for external use.
var seriesFeedPlusPlus = (function () {
    'use strict';

    var seriesFeedPlusPlus, configDialog, // Objects
        debug, pageRegexes, currentPage, flags, subProviders, dlProviders, dlFormats, dlSites, downloadImage,
        languageMap, // Variables
        main, checkPage, injectMenuItem, modifyPage, handleStartPage, injectDefaultTable, createFunctionality,
        createLanguageFlag, parseEpisode, showSubSelectionDialog, handleBroadcastPage, handleWatchlistPage,
        injectTableHeader, showDlSelectionDialog, createDownloadLink, formatToConvention; // Methods

    // Initialize objects
    seriesFeedPlusPlus = {};
    configDialog = (function () {
        var instance, configElementName, preferences, mapping, show, close, closeOtherSubConfigs, closeSubConfig,
            openSubConfig, changeConfiguration, saveConfiguration, loadPreferences, getEnabledSubtitleLanguages,
            getConfigValue, getEnabledSubtitleSources, getEnabledDownloadProviders, getEnabledDownloadTypes;

        // Init vars
        configElementName = "configFrame";
        // Preferences with their default values
        preferences = {
            sub_lang_nl: true,
            sub_lang_en: true,
            sub_source_addic7ed: true,
            sub_source_podnapisi: true,
            sub_source_opensubtitles: false,
            sub_source_subtitleseeker: false,
            dl_format_webdl: true,
            dl_format_hdtv: true,
            dl_format_hdtvx264: true,
            dl_source_torrent: false,
            dl_source_nzb: true
        };
        mapping = {
            sub_lang_nl: "Ext.SF.SubLanguage_NL",
            sub_lang_en: "Ext.SF.SubLanguage_US",
            sub_source_addic7ed: "Ext.SF.SubProvider_Addic7ed",
            sub_source_podnapisi: "Ext.SF.SubProvider_PodNapisi",
            sub_source_opensubtitles: "Ext.SF.SubProvider_OpenSubTitles",
            sub_source_subtitleseeker: "Ext.SF.SubProvider_SubtitleSeeker",
            dl_format_webdl: "Ext.SF.MediaFormat_WEB-DL",
            dl_format_hdtv: "Ext.SF.MediaFormat_HDTV",
            dl_format_hdtvx264: "Ext.SF.MediaFormat_HTDV.x264",
            dl_source_torrent: "Ext.SF.DownloadFormat_Torrent",
            dl_source_nzb: "Ext.SF.DownloadFormat_NZB"
        };
        // Initialize functions
        show = function () {
            var css, html, div, subFrames, idx, inputs;
            if (document.getElementById(configElementName)) {
                close();
                return;
            }
            css = ' ' +
                '.h3subframe { margin: 1px 0 0px; padding: 1px 10px; border-bottom: 1px solid #bbb; font-size: 1.5em; font-weight: normal; cursor:pointer; background:#DDDDDD none repeat scroll 0 0; } ' +
                '.h3subframe:hover { background:#C0BEBE none repeat scroll 0 0; } ' +
                '#h3subframetitle { margin: 2px 0 0px; padding: 7px 10px; border-bottom: 1px solid #bbb; font-size: 2.0em; font-weight: normal; } ' +
                '.popup a { color: darkblue; text-decoration: none; } ' +
                '.popup p { padding: 1px 10px; margin: 0px 0; font-family:verdana,geneva,lucida,"lucida grande",arial,helvetica,sans-serif; font-size:10pt; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal; line-height:normal; } ' +
                '.sidebyside { padding: 1px 10px; margin: 0px 0;display:inline-block;width:17em; } ' +
                '.h3subframecontent { max-height:294px; overflow:auto; display: none; padding: 10px 10px; } ' +
                '#showinfo { font-size:14px; } ';
            GM_addStyle(css);

            html =
                '<div id="fade" style="background: #000;height: 100%;opacity: .80;"></div>' +
                '<div style="font-family: verdana; color: black; background: #ddd; padding: 10px 20px; border: 10px solid #fff; float: left; width: 731px; position: absolute; top: 2%; left: 40%; margin: 0 0 0 -292px; border-radius: 10px; z-index: 100;">' +
                '    <div class="popup" style="float: left; width: 100%; background: #fff; margin: 10px 0; padding: 0px 0 0px; border-left: 1px solid #bbb; border-top: 1px solid #bbb; border-right: 1px solid #bbb;">' +
                '        <a href="#" onclick="javascript:return false;">' +
                '            <img id="' + configElementName + '_close" style="border:none; position: absolute; right: -20px; top: -20px;" title="Close" src="data:image/png;base64,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%3D%3D"/>' +
                '        </a>' +
                '        <div id="h3subframetitle"><b>SeriesFeed AddOn - Preferences</b></div>' +
                '        <div id="h3subframe1" class="h3subframe">Media</div>' +
                '        <div class="h3subframecontent">' +
                '            <p id="showinfo">Choose the <b>media formats</b> you need</p><br>' +
                '            <div class="sidebyside"><input type="checkbox" id="dl_format_webdl" /> WEB-DL <font color="gray">(720p &amp; 1080p)</font></div>' +
                '            <div class="sidebyside"><input type="checkbox" id="dl_format_hdtv" />  HDTV <font color="gray">(720p &amp; 1080p)</font></div>' +
                '            <div class="sidebyside"><input type="checkbox" id="dl_format_hdtvx264" />  HDTV_x264 <font color="gray">(MP4)</font></div>' +
                '            <hr/>' +
                '            <p id="showinfo">Choose the <b>type of files</b> you want to download</p><br>' +
                '            <div class="sidebyside"><input type="checkbox" id="dl_source_nzb" /> NZB</div>' +
                '            <div class="sidebyside"><input type="checkbox" id="dl_source_torrent" /> Torrent</div><br>' +
                '        </div>' +
                '        <div id="h3subframe2" class="h3subframe">Subtitles</div>' +
                '        <div class="h3subframecontent">' +
                '            <p id="showinfo">Choose the <b>subtitle sites</b> you want as option</p><br>' +
                '            <p><input type="checkbox" id="sub_source_addic7ed" /> Addic7eD <font color="gray">(preferred)</font></p>' +
                '            <p><input type="checkbox" id="sub_source_podnapisi" /> PodNapisi</p>' +
                '            <p><input type="checkbox" id="sub_source_opensubtitles" /> OpenSubtitles</p>' +
                '            <p><input type="checkbox" id="sub_source_subtitleseeker" /> SubTitleSeeker <font color="gray">(can be unsafe)</font></p>' +
                '        </div>' +
                '        <div id="h3subframe3" class="h3subframe">Languages</div>' +
                '        <div class="h3subframecontent">' +
                '            <p id="showinfo">Choose the <b>subtitle languages</b> you want to find</p><br>' +
                '            <p><input type="checkbox" id="sub_lang_nl" /> Nederlands <img src="' + flags.nl + '"/></p>' +
                '            <p><input type="checkbox" id="sub_lang_en" /> English <img src="' + flags.en + '"/></p>' +
                '        </div>' +
                '        <div id="h3subframe4" class="h3subframe">About</div>' +
                '        <div class="h3subframecontent">' +
                '            <p><b>' + GM_info.script.name + '</b> - version: ' + GM_info.script.version + '</p>' +
                '            <br />' +
                '            <p>' + GM_info.script.description + '</p>' +
                '            <p>Author: Mr. Invisible (original author: XppX)</p>' +
                '            <p>License: GPL</p>' +
                '        </div>' +
                '    </div>' +
                '</div>';

            div = document.createElement("div");
            div.id = configElementName;
            div.setAttribute('style',
                'visibility: visible;position: fixed;width: 100%;height: 100%;top: 0;left: 0;font-size:12px;' +
                'z-index:1001;text-align:left;');
            div.innerHTML = html;
            document.body.appendChild(div);
            document.getElementById(configElementName + "_close").addEventListener("click", close, false);

            // Loop through checkboxes to populate them
            inputs = div.getElementsByTagName("input");
            for (idx = 0; idx < inputs.length; idx++) {
                if (inputs[idx].type === "checkbox") {
                    if (preferences.hasOwnProperty(inputs[idx].id) && preferences[inputs[idx].id]) {
                        inputs[idx].setAttribute("checked", "checked");
                    }
                    // Add a listener to each checkbox
                    inputs[idx].addEventListener("click", changeConfiguration, false);
                }
            }

            // Add event listeners for opening when a click on the head is performed
            subFrames = document.getElementsByClassName("h3subframe");
            for (idx = 0; idx < subFrames.length; idx++) {
                subFrames[idx].addEventListener("click", openSubConfig, false);
            }
            // Unfold the first one
            openSubConfig({
                target: document.getElementById('h3subframe1')
            });
        };
        close = function () {
            var box = document.getElementById(configElementName);
            box.parentNode.removeChild(box);
            window.location.reload(false);
        };
        closeOtherSubConfigs = function (evt) {
            var ignore, subFrames, idx;
            ignore = evt.target || evt.srcElement;
            subFrames = document.getElementsByClassName("h3subframe");
            for (idx = 0; idx < subFrames.length; idx++) {
                if (ignore !== subFrames[idx]) {
                    subFrames[idx].nextElementSibling.style.display = "none";
                    subFrames[idx].addEventListener("click", openSubConfig, false);
                }
            }
        };
        closeSubConfig = function (e) {
            var evt, target;

            evt = e || window.event;
            target = evt.target || evt.srcElement;
            target.removeEventListener("click", closeSubConfig, false);
            target.nextElementSibling.style.display = "none";
            target.addEventListener("click", openSubConfig, false);
        };
        openSubConfig = function (e) {
            var evt, target;

            evt = e || window.event;
            target = evt.target || evt.srcElement;
            target.removeEventListener("click", openSubConfig, false);
            target.nextElementSibling.style.display = "block";
            closeOtherSubConfigs(evt);
            target.addEventListener("click", closeSubConfig, false);
        };
        changeConfiguration = function (e) {
            if (e.target.tagName.toLowerCase() === 'input') {
                saveConfiguration(e.target.id, e.target.checked);
            }
        };
        saveConfiguration = function (id, value) {
            if (preferences.hasOwnProperty(id)) {
                preferences[id] = value;
                GM_setValue(mapping[id], value);
            }
        };
        loadPreferences = function () {
            var key;
            if (debug) {
                window.console.log("Entering load preferences function");
            }
            if (debug) {
                window.console.log("Preferences (default):");
                window.console.log(preferences);
            }

            for (key in mapping) {
                if (mapping.hasOwnProperty(key) && preferences.hasOwnProperty(key)) {
                    preferences[key] = GM_getValue(mapping[key], preferences[key]);
                }
            }

            if (debug) {
                window.console.log("Preferences (loaded):");
                window.console.log(preferences);
            }
        };
        getConfigValue = function (name) {
            if (preferences.hasOwnProperty(name)) {
                return preferences[name];
            }
            return null;
        };
        getEnabledSubtitleLanguages = function () {
            var result = [];

            if (preferences.sub_lang_en) {
                result.push("en");
            }
            if (preferences.sub_lang_nl) {
                result.push("nl");
            }

            return result;
        };
        getEnabledSubtitleSources = function () {
            var result = [];

            if (preferences.sub_source_addic7ed) {
                result.push(subProviders.sub_source_addic7ed);
            }
            if (preferences.sub_source_podnapisi) {
                result.push(subProviders.sub_source_podnapisi);
            }
            if (preferences.sub_source_opensubtitles) {
                result.push(subProviders.sub_source_opensubtitles);
            }
            if (preferences.sub_source_subtitleseeker) {
                result.push(subProviders.sub_source_subtitleseeker);
            }

            return result;
        };
        getEnabledDownloadProviders = function () {
            var result = [];

            if (preferences.dl_source_torrent) {
                result.push(dlProviders.dl_source_torrent);
            }
            if (preferences.dl_source_nzb) {
                result.push(dlProviders.dl_source_nzb);
            }

            return result;
        };
        getEnabledDownloadTypes = function () {
            var result = [];

            if (preferences.dl_format_webdl) {
                result.push(dlFormats.dl_format_webdl);
            }
            if (preferences.dl_format_hdtv) {
                result.push(dlFormats.dl_format_hdtv);
            }
            if (preferences.dl_format_hdtvx264) {
                result.push(dlFormats.dl_format_hdtvx264);
            }

            return result;
        };
        // Initialize object to return and expose appropriate methods
        instance = {};
        instance.show = show;
        instance.loadPreferences = loadPreferences;
        instance.getConfigValue = getConfigValue;
        instance.getEnabledSubtitleLanguages = getEnabledSubtitleLanguages;
        instance.getEnabledSubtitleSources = getEnabledSubtitleSources;
        instance.getEnabledDownloadProviders = getEnabledDownloadProviders;
        instance.getEnabledDownloadTypes = getEnabledDownloadTypes;

        return instance;
    }());
    // Initialize variables
    debug = false;
    // Maps short language keywords to the full English language
    languageMap = {
        "en": "English",
        "nl": "Dutch"
    };
    // Providers, keys of this MUST be equal to the ones in the configDialog.preferences variable
    subProviders = {
        sub_source_addic7ed: {
            title: "Addic7eD",
            createLink: function (showName, showEpisode, language) {
                var showNameConverted, showEpisodeConverted, languageConverted;
                // Convert show name & show episode to appropriate formats
                showNameConverted = this.showConversion(showName);
                showEpisodeConverted = this.episodeConversion(showEpisode);
                languageConverted = this.languageConversion(language);

                return "http://www.addic7ed.com/serie/" + showNameConverted + "/" + showEpisodeConverted.season + "/" +
                    showEpisodeConverted.episode + "/" + languageConverted;
            },
            showConversion: function (show) {
                var exceptions;

                show = show.replace(" ", "_");
                // Exception map for shows
                exceptions = {
                    "The_Flash": "The_Flash_(2014)"
                };
                if (exceptions.hasOwnProperty(show)) {
                    show = exceptions[show];
                }
                return show;
            },
            episodeConversion: function (episode) { return parseEpisode(episode); },
            languageConversion: function (language) {
                switch (language) {
                case "nl":
                    return "17";
                case "en":
                    return "1";
                }
                return language;
            }
        },
        sub_source_podnapisi: {
            title: "PodNapisi",
            createLink: function (showName, showEpisode, language) {
                var showNameConverted, showEpisodeConverted, languageConverted;
                // Convert show name & show episode to appropriate formats
                showNameConverted = this.showConversion(showName);
                showEpisodeConverted = this.episodeConversion(showEpisode);
                languageConverted = this.languageConversion(language);

                return "http://www.podnapisi.net/subtitles/search/advanced?keywords=" + showNameConverted + "&seasons="
                    + showEpisodeConverted.season + "&episodes=" + showEpisodeConverted.episode + "&language=" +
                    languageConverted;
            },
            showConversion: function (show) {
                var exceptions;

                show = show.replace(" ", "+");
                // Exception map for shows
                exceptions = {};
                if (exceptions.hasOwnProperty(show)) {
                    show = exceptions[show];
                }
                return show;
            },
            episodeConversion: function (episode) { return parseEpisode(episode); },
            languageConversion: function (language) { return language; }
        },
        sub_source_opensubtitles: {
            title: "OpenSubtitles",
            createLink: function (showName, showEpisode, language) {
                var showNameConverted, showEpisodeConverted, languageConverted;
                // Convert show name & show episode to appropriate formats
                showNameConverted = this.showConversion(showName);
                showEpisodeConverted = this.episodeConversion(showEpisode);
                languageConverted = this.languageConversion(language);

                return "http://www.openSubtitles.org/nl/search/searchonlytvseries-on/subformat-srt/sublanguageid-" +
                    languageConverted + "/season-" + showEpisodeConverted.season + "/episode-" +
                    showEpisodeConverted.episode + "/moviename-" + showNameConverted;
            },
            showConversion: function (show) {
                var exceptions;

                show = show.replace(" ", "+");
                // Exception map for shows
                exceptions = {};
                if (exceptions.hasOwnProperty(show)) {
                    show = exceptions[show];
                }
                return show;
            },
            episodeConversion: function (episode) { return parseEpisode(episode); },
            languageConversion: function (language) {
                switch (language) {
                case "nl":
                    return "dut";
                case "en":
                    return "eng";
                }
            }
        },
        sub_source_subtitleseeker: {
            title: "SubTitleSeeker",
            createLink: function (showName, showEpisode, language) {
                var showNameConverted, showEpisodeConverted, convertedLanguage;
                // Convert show name & show episode to appropriate formats
                showNameConverted = this.showConversion(showName);
                showEpisodeConverted = this.episodeConversion(showEpisode);
                convertedLanguage = this.languageConversion(language);
                if (debug) {
                    window.console.log("Language is not used for subTitleSeeker: " + convertedLanguage);
                }

                return "http://www.subtitleseeker.com/search/TV_EPISODES/" + showNameConverted + "+S" +
                    showEpisodeConverted.season + "E" + showEpisodeConverted.episode;
            },
            showConversion: function (show) {
                var exceptions;

                show = show.replace(" ", "+");
                // Exception map for shows
                exceptions = {};
                if (exceptions.hasOwnProperty(show)) {
                    show = exceptions[show];
                }
                return show;
            },
            episodeConversion: function (episode) { return parseEpisode(episode); },
            languageConversion: function (language) { return language; }
        }
    };
    dlFormats = {
        dl_format_webdl: "WEB-DL 1080/720p",
        dl_format_hdtv: "HDTV 1080/720p",
        dl_format_hdtvx264: "HDTV-x264 (MP4)"
    };
    dlSites = {
        torrentz: {
            title: "Torrentz",
            createLink: function (showName, showEpisode, quality, dialog) {
                var url, dlAddition;

                // Adjust name & episode for search
                showName = showName.replace(" ", "+");
                showEpisode = parseEpisode(showEpisode);
                // Determine addition based on quality
                dlAddition = "";
                switch (quality) {
                case dlFormats.dl_format_webdl:
                    dlAddition = "+WEB%20DL";
                    break;
                case dlFormats.dl_format_hdtv:
                    dlAddition = "+720p+x264";
                    break;
                case dlFormats.dl_format_hdtvx264:
                    dlAddition = "+HDTV+x264";
                    break;
                default:
                    window.console.warn("Got an unknown quality type: " + quality);
                }

                url = "https://torrentz.eu/search?f=" + showName + "+" + formatToConvention(showEpisode) + dlAddition;
                return createDownloadLink(this.title, url, dialog);
            }
        },
        kat: {
            title: "KickAssTorrents",
            createLink: function (showName, showEpisode, quality, dialog) {
                var url, dlAddition;

                // Adjust name & episode for search
                showName = showName.replace(" ", "%2B");
                showEpisode = parseEpisode(showEpisode);
                // Determine addition based on quality
                dlAddition = "";
                switch (quality) {
                case dlFormats.dl_format_webdl:
                    dlAddition = "%2BWEB-DL";
                    break;
                case dlFormats.dl_format_hdtv:
                    dlAddition = "%2B720p%2Bx264";
                    break;
                case dlFormats.dl_format_hdtvx264:
                    dlAddition = "%2BHDTV.x264";
                    break;
                default:
                    window.console.warn("Got an unknown quality type: " + quality);
                }

                url = "https://kat.cr/usearch/" + showName + "%2B" + formatToConvention(showEpisode) + dlAddition;
                return createDownloadLink(this.title, url, dialog);
            }
        },
        nzbindex: {
            title: "NZBIndex",
            createLink: function (showName, showEpisode, quality, dialog) {
                var url, dlAddition;

                // Adjust name & episode for search
                showName = showName.replace(" ", "+");
                showEpisode = parseEpisode(showEpisode);
                // Determine addition based on quality
                dlAddition = "";
                switch (quality) {
                case dlFormats.dl_format_webdl:
                    dlAddition = "+0p++WEB-DL&age=&max=25&sort=agedesc&minsize=600&maxsize=5120&poster=&nfo=&hidespam=1&more=0";
                    break;
                case dlFormats.dl_format_hdtv:
                    dlAddition = "+0p++x264&age=&max=25&sort=agedesc&minsize=600&maxsize=5120&poster=&nfo=&hidespam=1&more=0";
                    break;
                case dlFormats.dl_format_hdtvx264:
                    dlAddition = "+HDTV.x264&age=&max=25&sort=agedesc&minsize=150&maxsize=1536&poster=&nfo=&hidespam=1&more=0";
                    break;
                default:
                    window.console.warn("Got an unknown quality type: " + quality);
                }

                url = "https://www.nzbindex.com/search/?q=" + showName + '+"' + formatToConvention(showEpisode) +
                    '"|"' + formatToConvention(showEpisode, "x") + dlAddition;
                return createDownloadLink(this.title, url, dialog);
            }
        },
        nzbclub: {
            title: "NZBClub",
            createLink: function (showName, showEpisode, quality, dialog) {
                var url, dlAddition;

                // Adjust name & episode for search
                showName = showName.replace(" ", "+");
                showEpisode = parseEpisode(showEpisode);
                // Determine addition based on quality
                dlAddition = "";
                switch (quality) {
                case dlFormats.dl_format_webdl:
                    dlAddition = "+WEB-DL&szs=20&sze=24&st=1&sp=1&sn=1";
                    break;
                case dlFormats.dl_format_hdtv:
                    dlAddition = "+0p+x264&szs=20&sze=24&st=1&sp=1&sn=1";
                    break;
                case dlFormats.dl_format_hdtvx264:
                    dlAddition = "+HDTV.x264&sz=16&ez=22&rpp=25&st=1&sp=1&sn=1";
                    break;
                default:
                    window.console.warn("Got an unknown quality type: " + quality);
                }

                url = "https://www.nzbclub.com/search.aspx?q=" + showName + '+' + formatToConvention(showEpisode) +
                    dlAddition;
                return createDownloadLink(this.title, url, dialog);
            }
        },
        binsearch: {
            title: "BinSearch",
            createLink: function (showName, showEpisode, quality, dialog) {
                var url, dlAddition;

                // Adjust name & episode for search
                showName = showName.replace(" ", "+");
                showEpisode = parseEpisode(showEpisode);
                // Determine addition based on quality
                dlAddition = "";
                switch (quality) {
                case dlFormats.dl_format_webdl:
                    dlAddition = "+WEB-DL&m=&max=25&adv_g=&adv_age=999&adv_sort=date&adv_col=on&minsize=600&maxsize=5120&font=small&postdate=";
                    break;
                case dlFormats.dl_format_hdtv:
                    dlAddition = "+0p+x264&m=&max=25&adv_g=&adv_age=999&adv_sort=date&adv_col=on&minsize=600&maxsize=5120&font=small&postdate=";
                    break;
                case dlFormats.dl_format_hdtvx264:
                    dlAddition = "+HDTV.x264&m=&max=25&adv_g=&adv_age=999&adv_sort=date&adv_col=on&minsize=150&maxsize=1024&font=small&postdate=";
                    break;
                default:
                    window.console.warn("Got an unknown quality type: " + quality);
                }

                url = "https://binsearch.info/index.php?q=" + showName + '+' + formatToConvention(showEpisode) +
                    dlAddition;
                return createDownloadLink(this.title, url, dialog);
            }
        }
    };
    dlProviders = {
        dl_source_torrent: {
            title: "Torrent",
            sites: [
                dlSites.torrentz,
                dlSites.kat
            ]
        },
        dl_source_nzb: {
            title: "NZB",
            sites: [
                dlSites.nzbindex,
                dlSites.nzbclub,
                dlSites.binsearch
            ]
        }
    };
    // Flags
    flags = {
        "nl": "data:image/gif;base64,R0lGODlhEAALANUAAAABdP7+/jRusgAhj/picxZYp/UAAPLy8vr6+vj4+Pb29v56iVqKw/1da0V7ujpytSpmrusAAGSSx/pCUftUZP11gwAAR/96hgAAWe7v7yFgq0J3tw1Rokt/vAAAPFCCv1SFwS9qrtHR0fk8Tf6CkdXV1ftqevcxQvg3Rz52uPxMWvVWavZabv8VMflXaP5ZZ3eezv6XpC5qsvYsPPv7+/pQX/5/j/htfvlIV/T19fxwgPX19fz8/P0AAPT09P8AACH5BAAAAAAALAAAAAAQAAsAAAaFwJ/w1ysWDUhkJPJbLC6Vis5kIhAaLkqNeWk0XloVbjJCnWa4SCtGsjmltyqBtbpFDoE8j0dDIBIJCjs+OyUZenx+gII+Bzkih3t9f4GDBwc+IgMwDCAfHQ4bDwIhEBAaGxYAEg4pozKnGgUFHBwhHqsMnZ+hrgKkELgAwxjFxRbIHsoeQQA7",
        "en": "data:image/gif;base64,R0lGODlhEAALANUAAGNjtvr6+vb29fHx8fxGRvkQEP15eatjSuNjSjx6+TV0+f2Kd+np6dvb2PxUVPx4YttjSvsrK5q7/PxlZrpjSvw6Oubm5e7u7sljSgR09Z1jSvT080iB+gBMtkF9+vofHwBpu/f39uLi3/syMtVjSqG+/C9y+Ozs7KLA/GNlwPyBbO3t6gNx5QArs/2XhpGRgKysn+z+/rS0qPR0XuXl/P7KwcnJwf39Wzh3+f7+/fv/+vHx7wNv4df+/f3h3GNnxCH5BAAAAAAALAAAAAAQAAsAAAaMwJ/wl0oBjqoHYgmB/HIoXSkgoQYCoawA9kNxOJ5EAmcyTByEiouUmnoEiYspMxhcTreNrR1OKOYgaBUjEQsYAAEJAwonLCIddwwMFj0yABI4fzwgHS2DER8fCwciAQIbG3aRkxYNPi8qNWZoaRGgBbgqGg0BMVqoqTsrNBYiM0sITU0kGBQUBwca0kEAOw=="
    };
    downloadImage = "data:image/gif;base64,R0lGODlhEAAQAPeAAO3t7ezs7N/f37m5udLS0pSUlL6+vtvb28jIyOLi4t7e3ujo6Lu7u6CgoL+/v9HR0ZKSktnZ2cDAwMPDw5OTk7S0tKrLkdDQ0K3NlOHh4ba2tiZ6B9bW1tPT03SrT7W1tfPz87Ozs6qsqESPFmRkZJGRkcfHx4GzWsvLy466b1xhWWalSMTExGilRqampufn55qamjSJGMLCwjyLEMzMzJC8dF6fOtzr2I6Ojh57A/Hx8by8vG6kPIW2arDOltjY2KHHiqHGiqnKkM3Nza2trTaICbGxsVygPMHBwfT09GehNLi4uJbBgKjLkOPj4zOIC0aPGGOfMdXV1ZfCgpmZmWWeNW2jOEqRHNnp1a2vq+Hu3oW2Zl+hQzeJGyqCCm+lPY+Pj87OzoaGhlubKSZ/AWujN/X19enp6WlpaYK1Z4m4a57FiGWfMcPHwdfX11KXIq6urmGdMMnJyXSGb1eYKNzc3JjCg3yvVY2NjaioqKXIjYCzYc/Pz2KjRuDg4N3d3f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAIAALAAAAAAQABAAAAjdAAEJHEiwoMGDBhNIIRBGDgsHA0IQcTFgoBkFIABo1Bigow4jTgQKWHAgQYIMflIKmPCAQB5ASSIEUPCDAA0EJhCguPBnQYM/Bxa8QOlHgAAFf/7UOeBnCBU3fxAgkWCAwYAlGniwyVJBQoEJf2QYKAGhQAEKEKqcoCOCD5odEariAPPFipI4dyx4gNKGxAcOVjVUKOMDg4UmNYC0KKICzpkOBB5ciIIhhZote9LYOULGQQMYFPCIGSNET5A1Paas2DDH4JsrI2bYYNInBhaEA59w6XID90AvObQMDAgAOw==";
    // Page regexes
    pageRegexes = {
        // SeriesFeed homepage, when logged in, ends with "loggedin/"
        start: new RegExp("^.*/loggedin/$"),
        // Broadcast schedule (format: series/uitzendlijst/[{month}/]* )
        broadcast: new RegExp("^.*/series/uitzendlijst(/[a-z]+)*/$"),
        // Week overview (format: series/weekoverzicht/ )
        //week: new RegExp("^.*/series/weekoverzicht/$"),
        // Watchlist (format: series/kijklijst/[topshows/|favorieten/]*
        watch: new RegExp("^.*/series/kijklijst/([topshows|favorieten]+/)*$")//,
        // Episodes/Seasons (format: series/{name}/afleveringen/[seizoen/{nr}/]* )
        //season: new RegExp("^.*/series/(.+)/afleveringen/(seizoen/[0-9]+/)*$"),
        // Episode (format: series/aflevering/{nr}/ )
        //episode: new RegExp("^.*/series/aflevering/[0-9]+/$")
    };
    // Current page
    currentPage = null;

    // Initialize functions
    main = function () {
        if (debug) {
            window.console.log("Entering main function");
        }
        // Load preferences
        configDialog.loadPreferences();
        // Register GreaseMonkey menu entry
        try {
            GM_registerMenuCommand("[SeriesFeed AddOn] Configuratie", configDialog.show, "C");
        } catch (e) {
            window.console.warn("Could not register GM menu handler. This is normal if run outside of GreaseMonkey.", e);
        }
        // Inject config menu item
        injectMenuItem();
        // Check page we're on
        checkPage();
        // If the page is still null at this point, we didn't identify the page.
        if (currentPage === null) {
            window.console.warn("Did not identify a page to run on. Not executing any more page alterations.");
            return;
        }
        // Modify the page
        modifyPage();
    };
    checkPage = function () {
        var key, found;
        if (debug) {
            window.console.log("Entering checkPage function");
        }

        found = null;
        for (key in pageRegexes) {
            if (pageRegexes.hasOwnProperty(key)) {
                if (debug) {
                    window.console.log("Trying to match " + pageRegexes[key] + " to " + window.location.href);
                }
                if (pageRegexes[key].exec(window.location.href)) {
                    if (debug) {
                        window.console.log("Match found for " + key);
                    }
                    found = key;
                    break;
                }
            }
        }
        currentPage = found;
    };
    injectMenuItem = function () {
        var idx, links, li, menu, inject, injectLink;
        if (debug) {
            window.console.log("Entering injectMenuItem function");
        }
        // There are no id's used, so we'll hook on to some text contents in the page
        links = document.getElementsByTagName("a");
        for (idx = 0; idx < links.length; idx++) {
            if (links[idx].innerHTML === "Profiel wijzigen") {
                // Might have a match, verify "menu" element above
                li = links[idx].parentNode;
                menu = li.parentNode;
                if (menu.classList.contains("dropdown-menu")) {
                    // We can assume safely that we're in a menu. Inject menu item
                    inject = document.createElement("li");
                    injectLink = document.createElement("a");
                    injectLink.innerHTML = "SeriesFeed++ configureren";
                    injectLink.addEventListener("click", configDialog.show, false);
                    inject.appendChild(injectLink);
                    menu.appendChild(inject);
                }
            }
        }
    };
    modifyPage = function () {
        if (debug) {
            window.console.log("Entering modifyPage function");
        }
        // Depending on the type of the page, we need to render differently
        switch (currentPage) {
        case "start":
            handleStartPage();
            break;
        case "broadcast":
            handleBroadcastPage();
            break;
        case "watch":
            handleWatchlistPage();
            break;
        /*case "season":
            handleSeasonPage();
            break;
        case "episode":
            handleEpisodePage();
            break;*/
        default:
            window.console.warn("Did not identify a page to run on. Not executing any more page alterations.");
        }
        // Append css for jquery UI
        $("head").append('<link href="//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css"' +
            'rel="stylesheet" type="text/css">');
    };
    // Page specific modifications
    handleStartPage = function () {
        if (debug) {
            window.console.log("Entering handleStartPage function");
        }
        // There are two tables: last broadcasted favourites, and future episodes
        injectDefaultTable("favourite_episodes");
		injectDefaultTable("future_episodes");
    };
    handleBroadcastPage = function () {
        if (debug) {
            window.console.log("Entering handleBroadcastPage function");
        }
        // Single table: broadcasted episodes
        injectDefaultTable("afleveringen");
    };
    handleWatchlistPage = function () {
        if (debug) {
            window.console.log("Entering handleWatchlistPage function");
        }
        // Single table: favourites/popular episodes
        injectDefaultTable("afleveringen");
    };
    /*handleSeasonPage = function () {
        var table, showName, urlMatch;
        if (debug) {
            window.console.log("Entering handleSeasonPage function");
        }
        // Get show name
        urlMatch = pageRegexes.season.exec(window.location.href);
        showName = urlMatch[1];

        // Single table: show episodes
        table = $("#afleveringen");
        // Inject element for header
        injectTableHeader("afleveringen");
        // Inject icons in rows
        table.find("tbody tr.light").each(function (idx, elm) {
            var td, cells, showEpisode;
            if (debug) {
                window.console.log("Processing row " + idx);
            }
            td = document.createElement("td");
            cells = elm.getElementsByTagName("td");
            showEpisode = cells[0].firstChild.innerHTML;
            td.appendChild(createFunctionality(showName, showEpisode));
            elm.appendChild(td);
        });
    };
    handleEpisodePage = function () {
        if (debug) {
            window.console.log("Entering handleEpisodePage function");
        }

    };*/
    // General modification methods
    injectTableHeader = function (tableId) {
        var table, th;
        if (debug) {
            window.console.log("Entering injectTableHeader function");
        }

        table = $("#" + tableId);
        // Inject element for header
        th = document.createElement("th");
        th.innerHTML = "SeriesFeed++";
        table.find("thead tr")[0].appendChild(th);
    };
    injectDefaultTable = function (tableId) {
        var table;
        if (debug) {
            window.console.log("Entering injectDefaultTable function");
        }

        table = $("#" + tableId);
		// Check if element actually exists
		if(table.length === 0){
			if (debug) {
				window.console.log("Did not find a table with the id: " + tableId);
			}
			return;
		}
        // Inject element for header
        injectTableHeader(tableId);
        // Inject icons in rows
        table.find("tbody tr.light").each(function (idx, elm) {
            var td, cells, showName, showEpisode;
            if (debug) {
                window.console.log("Processing row" + idx);
            }
            td = document.createElement("td");
            cells = elm.getElementsByTagName("td");
            showName = cells[0].firstChild.innerHTML;
            showEpisode = cells[1].firstChild.innerHTML;
            td.appendChild(createFunctionality(showName, showEpisode));
            elm.appendChild(td);
        });
    };
    createFunctionality = function (showName, showEpisode) {
        var span, languages, idx, downloadProviders, downloadTypes, downloadImg;

        span = document.createElement("span");
        // Add language flags
        languages = configDialog.getEnabledSubtitleLanguages();
        for (idx = 0; idx < languages.length; idx++) {
            span.appendChild(createLanguageFlag(languages[idx], showName, showEpisode));
            span.appendChild(document.createTextNode(" "));
        }
        downloadProviders = configDialog.getEnabledDownloadProviders();
        downloadTypes = configDialog.getEnabledDownloadTypes();
        if (downloadProviders.length > 0 && downloadTypes.length > 0) {
            downloadImg = document.createElement("img");
            downloadImg.src = downloadImage;
            downloadImg.alt = "download";
            downloadImg.title = "download episode";
            downloadImg.addEventListener("click", showDlSelectionDialog, false);

            span.appendChild(downloadImg);
        }
        return span;
    };
    createLanguageFlag = function (lang, showName, showEpisode) {
        var result, img, subSources;

        if (!flags.hasOwnProperty(lang)) {
            throw new Error(lang + "is not a recognized language flag!");
        }

        img = document.createElement("img");
        img.src = flags[lang];
        img.alt = lang + " flag";
        img.title = languageMap[lang] + " subtitles";
        img.setAttribute("data-language", lang);
        // If there's just one subtitle source, make it a link, otherwise make it a pop-up menu
        subSources = configDialog.getEnabledSubtitleSources();
        if (subSources.length > 1) {
            img.addEventListener("click", showSubSelectionDialog, false);
            result = img;
        } else {
            result = document.createElement("a");
            result.href = subSources[0].createLink(showName, showEpisode, lang);
            result.target = "_blank";
            result.appendChild(img);
        }

        return result;
    };
    showSubSelectionDialog = function (e) {
        var evt, target, dialog, subSources, row, showName, showEpisode, lang, cells, idx, link, p;

        evt = e || window.event;
        target = evt.target || evt.srcElement;

        // Get language
        lang = target.getAttribute("data-language");
        // Get row, so we can extract show name & episode
        row = target.parentNode.parentNode.parentNode;
        cells = row.getElementsByTagName("td");
        showName = cells[0].firstChild.innerHTML;
        showEpisode = cells[1].firstChild.innerHTML;

        // Build dialog
        dialog = document.createElement("div");
        p = document.createElement("p");
        p.innerHTML = "Show: " + showName + "<br/>Episode: " + showEpisode;
        dialog.appendChild(p);
        p = document.createElement("p");
        // Get sub source sites
        subSources = configDialog.getEnabledSubtitleSources();
        for (idx = 0; idx < subSources.length; idx++) {
            link = document.createElement("a");
            link.target = "_blank";
            link.href = subSources[idx].createLink(showName, showEpisode, lang);
            link.innerHTML = subSources[idx].title;
            link.setAttribute("style","text-decoration: underline;");
			link.addEventListener("click", function(){ $(dialog).dialog( "close" ); }, false);
            p.appendChild(link);
            p.appendChild(document.createTextNode(" "));
        }
        dialog.appendChild(p);
        $(dialog).dialog({
            title: "Download " + languageMap[lang] + " subtitles",
            position: { my: "right bottom", at: "top left", of: target }
        });
    };
    showDlSelectionDialog = function (e) {
        var evt, target, dialog, row, cells, showName, showEpisode, p, downloadTypes, downloadProviders, idx, ul, li,
            jdx, kdx;

        evt = e || window.event;
        target = evt.target || evt.srcElement;

        // Get row, so we can extract show name & episode
        row = target.parentNode.parentNode.parentNode;
        cells = row.getElementsByTagName("td");
        showName = cells[0].firstChild.innerHTML;
        showEpisode = cells[1].firstChild.innerHTML;

        dialog = document.createElement("div");
        p = document.createElement("p");
        p.innerHTML = "Show: " + showName + "<br/>Episode: " + showEpisode;
        dialog.appendChild(p);
        p = document.createElement("p");
        // Get types & sites
        downloadProviders = configDialog.getEnabledDownloadProviders();
        downloadTypes = configDialog.getEnabledDownloadTypes();
        for (idx = 0; idx < downloadTypes.length; idx++) {
            p = document.createElement("p");
            p.appendChild(document.createTextNode(downloadTypes[idx]));
            dialog.appendChild(p);
            ul = document.createElement("ul");
            for (jdx = 0; jdx < downloadProviders.length; jdx++) {
                li = document.createElement("li");
                li.appendChild(document.createTextNode(downloadProviders[jdx].title + ": "));
                for (kdx = 0; kdx < downloadProviders[jdx].sites.length; kdx++) {
                    li.appendChild(downloadProviders[jdx].sites[kdx].createLink(showName, showEpisode, downloadTypes[idx], dialog));
                    li.appendChild(document.createTextNode(" "));
                }
                ul.appendChild(li);
            }
            dialog.appendChild(ul);
        }

        $(dialog).dialog({
            title: "Download episode",
            position: { my: "right bottom", at: "top left", of: target }
        });
    };
    parseEpisode = function (showEpisode) {
        var result, regex, match;
        if (debug) {
            window.console.log("Entering parseEpisode function");
        }

        result = {
            season: 0,
            episode: 0,
            title: ""
        };
        regex = new RegExp("S([0-9]+)E([0-9]+) - (.+)");
        // Epected format: SxEy - episode title
        match = regex.exec(showEpisode);
        if (match !== null) {
            result.season = parseInt(match[1], 10);
            result.episode = parseInt(match[2], 10);
            result.title = match[3];
        } else {
            window.console.warn("Could not parse " + showEpisode + " correctly!");
        }
        return result;
    };
    createDownloadLink = function (name, href, dialog) {
        var a;

        a = document.createElement("a");
        a.href = href;
        a.target = "_blank";
        a.innerHTML = name;
        a.setAttribute("style","text-decoration: underline;");
		a.addEventListener("click", function(){ $(dialog).dialog( "close" ); }, false);

        return a;
    };
    // Helper functions
    formatToConvention = function (episodeData, episodeCharacter) {
        episodeCharacter = episodeCharacter || "E";
        return "S" + ((episodeData.season < 10) ? "0" : "") + episodeData.season + episodeCharacter +
            ((episodeData.episode < 10) ? "0" : "") + episodeData.episode;
    };

    // Expose methods to the outside world
    seriesFeedPlusPlus.main = main;

    return seriesFeedPlusPlus;
}());

// Execute main
try {
    seriesFeedPlusPlus.main();
} catch (e) {
    // Display error
    var txt = "An error occurred while executing this script.\n\n";
    txt += "Issue: <<<" + e.message + ">>>\n\n";
    txt += "\nPlease report this back to the author (on the greasyfork website) so it can be corrected.\n\n";
    txt += "Click 'OK' to continue.\n\n";
    window.alert(txt);
}