Seriesfeed++

A fork of Bierdopje AddOn Plus for Seriesfeed

  1. // ==UserScript==
  2. // @name Seriesfeed++
  3. // @namespace https://greasyfork.org/en/users/22592
  4. // @description A fork of Bierdopje AddOn Plus for Seriesfeed
  5. // @include https://www.seriesfeed.com/*
  6. // @version 2.01
  7. // @grant GM_setValue
  8. // @grant GM_getValue
  9. // @grant GM.setValue
  10. // @grant GM.getValue
  11. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  12. // @require https://code.jquery.com/jquery-3.2.1.js
  13. // @require https://code.jquery.com/ui/1.12.1/jquery-ui.js
  14. // @author Mr. Invisible (mrinvisible@cryptolab.net)
  15. // @run-at document-end
  16. // ==/UserScript==
  17.  
  18. /*global GM.getValue,GM.info,GM.setValue,$ */
  19.  
  20. /**
  21. Changelog:
  22. 2.01:
  23. - Add pointer cursor to flags and menu entry
  24. - Fix displaying on the episodes overview page
  25. - Fix displaying on the episodes details page
  26. - Fixed issue with aync loading of preferences causing wrong settings
  27. - Leaves debug on to debug other possible issues
  28. 2.00: New GM API for the new FF version.
  29. 1.17:
  30. - Fix urls for real
  31. - Update JQuery version
  32. - Remove unneeded includes
  33. 1.16: Fix urls
  34. 1.15: Make compatible with https
  35. 1.14: Fix bug with provider introduced in 1.13
  36. 1.13: Added another search engine
  37. 1.12:
  38. - Fix problem with watchlist that have no items left
  39. - Replaced two dead search engines
  40. 1.11: Quick-fix for dialog
  41. 1.10:
  42. - Replaced all providers, now you can define your own
  43. - Replaced quality, now you can define your own
  44. 1.09: Added an exception
  45. 1.08: Multi-domain
  46. 1.07: Forgot to turn of debug once again
  47. 1.06:
  48. - Updated for Seriesfeed 2.0
  49. - Added new provider
  50. - Dialog also closes when middle-clicking
  51. - Re-added functionality to the episode and season pages.
  52. - Added exception for legends of tomorrow
  53. - Added email address for easier communication
  54. 1.05: Updated for Seriesfeed 1.3
  55. 1.04: Fixed problem with the visual watchlist & dialog for download now closes after clicking a link.
  56. 1.03: Updated for Seriesfeed 1.2
  57. 1.02: Fixed small bug with Chrome-derived browsers
  58. 1.01: Rewrote script in order to accommodate the Seriesfeed pages
  59. 1.00: Cloned from the Bierdopje AddOn Plus version 1.101
  60. **/
  61.  
  62. // Create one accessible object. The remainder is hidden for external use.
  63. var seriesFeedPlusPlus = (function () {
  64. 'use strict';
  65.  
  66. var seriesFeedPlusPlus, configDialog, // Objects
  67. debug, pageRegexes, currentPage, flags, subProviders, languageMap, // Variables
  68. main, checkPage, injectMenuItem, modifyPage, handleStartPage, injectDefaultTable, createFunctionality,
  69. createLanguageFlag, parseEpisode, showSubSelectionDialog, handleBroadcastPage, handleWatchlistPage,
  70. injectTableHeader, showDlSelectionDialog, createMediaLink, formatToConvention, handleSeasonPage,
  71. handleEpisodePage; // Methods
  72.  
  73. // Initialize objects
  74. seriesFeedPlusPlus = {};
  75. configDialog = (function () {
  76. var instance, configElementName, preferences, mapping, show, close, closeOtherSubConfigs, closeSubConfig,
  77. openSubConfig, changeConfiguration, saveConfiguration, loadPreferences, getEnabledSubtitleLanguages,
  78. getConfigValue, getEnabledSubtitleSources, getEnabledDownloadProviders, getEnabledMediaQualities,
  79. checkConfiguration, isValidQualityConfig, isValidProviderConfig, isValidProvidersConfig;
  80.  
  81. // Init vars
  82. configElementName = "configFrame";
  83. // Preferences with their default values
  84. preferences = {
  85. sub_lang_nl: true,
  86. sub_lang_en: true,
  87. sub_source_addic7ed: true,
  88. sub_source_podnapisi: true,
  89. sub_source_opensubtitles: false,
  90. sub_source_subtitleseeker: false,
  91. dl_quality: [
  92. 'WEB-DL', 'HDTV 1080', 'HDTV 720', 'x265'
  93. ],
  94. dl_providers: [
  95. {
  96. "name": "1337x",
  97. "url": "https://1337x.to/search/{show}+{season_episode}+{quality}/1/",
  98. "quality": {
  99. "WEB-DL": "WEB-DL",
  100. "HDTV 1080": "1080p x264",
  101. "HDTV 720": "720p x264",
  102. "x265": "x265"
  103. },
  104. "invalid_characters": {
  105. "old": ["(", ")", " "],
  106. "new": ["", "", "+"]
  107. },
  108. },
  109. {
  110. "name": "RARBG",
  111. "url": "https://rarbg.to/torrents.php?search={show} {season_episode} {quality}",
  112. "quality": {
  113. "WEB-DL": "WEB-DL",
  114. "HDTV 1080": "1080p x264",
  115. "HDTV 720": "720p x264",
  116. "x265": "x265"
  117. }
  118. },
  119. {
  120. "name": "TPB",
  121. "url": "https://thepiratebay.org/search/{show} {season_episode} {quality}",
  122. "quality": {
  123. "WEB-DL": "WEB-DL",
  124. "HDTV 1080": "1080p x264",
  125. "HDTV 720": "720p x264",
  126. "x265": "x265"
  127. }
  128. },
  129. {
  130. "name": "NZBIndex",
  131. "url": "https://www.nzbindex.com/search/?q={show} {season_episode} {quality}&max=25&sort=agedesc&hidespam=1&more=0",
  132. "quality": {
  133. "WEB-DL": "720p|1080p WEB-DL",
  134. "HDTV 1080": "1080p x264",
  135. "HDTV 720": "720p x264",
  136. "x265": "x265"
  137. }
  138. },
  139. {
  140. "name": "NZBClub",
  141. "url": "https://www.nzbclub.com/search.aspx?q={show} {season_episode} {quality}&szs=20&sze=24&st=1&sp=1&sn=1",
  142. "quality": {
  143. "WEB-DL": "WEB-DL",
  144. "HDTV 1080": "1080p x264",
  145. "HDTV 720": "720p x264",
  146. "x265": "x265"
  147. }
  148. },
  149. {
  150. "name": "BinSearch",
  151. "url": "https://binsearch.info/index.php?q={show} {season_episode} {quality}&max=25&adv_age=999&adv_sort=date&adv_col=on&font=small",
  152. "quality": {
  153. "WEB-DL": "720p|1080p WEB-DL",
  154. "HDTV 1080": "1080p x264",
  155. "HDTV 720": "720p x264",
  156. "x265": "x265"
  157. }
  158. }
  159. ]
  160. };
  161. mapping = {
  162. sub_lang_nl: "Ext.SF.SubLanguage_NL",
  163. sub_lang_en: "Ext.SF.SubLanguage_US",
  164. sub_source_addic7ed: "Ext.SF.SubProvider_Addic7ed",
  165. sub_source_podnapisi: "Ext.SF.SubProvider_PodNapisi",
  166. sub_source_opensubtitles: "Ext.SF.SubProvider_OpenSubTitles",
  167. sub_source_subtitleseeker: "Ext.SF.SubProvider_SubtitleSeeker",
  168. dl_providers: "Ext.SF.MediaProviders",
  169. dl_quality: "Ext.SF.MediaQuality"
  170. };
  171. // Initialize functions
  172. show = function () {
  173. var css, html, div, subFrames, idx, inputs, head, style;
  174. if (document.getElementById(configElementName)) {
  175. close();
  176. return;
  177. }
  178. head = document.getElementsByTagName('head')[0];
  179. style = document.createElement('style');
  180. style.setAttribute('type', 'text/css');
  181. style.textContent = ' ' +
  182. '.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; } ' +
  183. '.h3subframe:hover { background:#C0BEBE none repeat scroll 0 0; } ' +
  184. '#h3subframetitle { margin: 2px 0 0px; padding: 7px 10px; border-bottom: 1px solid #bbb; font-size: 2.0em; font-weight: normal; } ' +
  185. '.popup a { color: darkblue; text-decoration: none; } ' +
  186. '.popup p { padding: 1px 10px; margin: 0px 0; font-family: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; } ' +
  187. '.sidebyside { padding: 1px 10px; margin: 0px 0;display:inline-block;width:17em; } ' +
  188. '.h3subframecontent { overflow:auto; display: none; padding: 10px 10px; } ' +
  189. '.showinfo { font-size:14px; } ' +
  190. 'textarea.valid, textarea.valid:focus { border: 2px solid green; } ' +
  191. 'textarea.invalid, textarea.invalid:focus { border: 2px solid red; }';
  192. head.appendChild(style);
  193.  
  194. html =
  195. '<div id="fade" style="background: #000;height: 100%;opacity: .80;"></div>' +
  196. '<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;">' +
  197. ' <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;">' +
  198. ' <a href="#" onclick="javascript:return false;">' +
  199. ' <img id="' + configElementName + '_close" style="border:none; position: absolute; right: -20px; top: -20px;" src="data:image/png;base64,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%3D%3D"/>' +
  200. ' </a>' +
  201. ' <div id="h3subframetitle"><b>Seriesfeed++ - Preferences</b></div>' +
  202. ' <div id="h3subframe1" class="h3subframe">Languages</div>' +
  203. ' <div class="h3subframecontent">' +
  204. ' <p class="showinfo">Choose the <b>subtitle languages</b> you want to find</p><br>' +
  205. ' <p><input type="checkbox" id="sub_lang_nl" /> Nederlands <img src="' + flags.nl + '"/></p>' +
  206. ' <p><input type="checkbox" id="sub_lang_en" /> English <img src="' + flags.en + '"/></p>' +
  207. ' </div>' +
  208. ' <div id="h3subframe2" class="h3subframe">Subtitles</div>' +
  209. ' <div class="h3subframecontent">' +
  210. ' <p class="showinfo">Choose the <b>subtitle sites</b> you want as option</p><br>' +
  211. ' <p><input type="checkbox" id="sub_source_addic7ed" /> Addic7eD <font color="gray">(preferred)</font></p>' +
  212. ' <p><input type="checkbox" id="sub_source_podnapisi" /> PodNapisi</p>' +
  213. ' <p><input type="checkbox" id="sub_source_opensubtitles" /> OpenSubtitles</p>' +
  214. ' <p><input type="checkbox" id="sub_source_subtitleseeker" /> SubTitleSeeker <font color="gray">(can be unsafe)</font></p>' +
  215. ' </div>' +
  216. ' <div id="h3subframe3" class="h3subframe">Media</div>' +
  217. ' <div class="h3subframecontent">' +
  218. ' <p class="showinfo">For examples of the configuration, see the <a target="_blank" href="https://greasyfork.org/en/scripts/14722-seriesfeed">Greasyfork</a> website</p>' +
  219. ' <p class="showinfo">Enter the <b>media formats</b> you want to have links for</p><br>' +
  220. ' <textarea cols="100" rows="2" class="valid" id="config_dl_quality">' + JSON.stringify(preferences.dl_quality, null, 1) + '</textarea><br>Config will not be stored unless the border is green.<br><br>' +
  221. ' <p class="showinfo">Enter the configuration for the <b>media providers</b> you want to use</p><br>' +
  222. ' <textarea cols="100" rows="20" class="valid" id="config_dl_providers">' + JSON.stringify(preferences.dl_providers, null, '\t') + '</textarea><br>Config will not be stored unless the border is green.<br><br>' +
  223. ' </div>' +
  224. ' <div id="h3subframe4" class="h3subframe">About &amp; Help</div>' +
  225. ' <div class="h3subframecontent">' +
  226. ' <p><b>' + GM.info.script.name + '</b> - version: ' + GM.info.script.version + '</p>' +
  227. ' <br />' +
  228. ' <p>' + GM.info.script.description + '</p><br>' +
  229. ' <p>Author: Mr. Invisible (mrinvisible@cryptolab.net) - original plugin by: XppX</p>' +
  230. ' <p>License: GPL</p><br><br>' +
  231. ' <p><b>In need of help</b>? Visit the script page on <a target="_blank" href="https://greasyfork.org/en/scripts/14722-seriesfeed">Greasyfork</a>.</p>' +
  232. ' </div>' +
  233. ' </div>' +
  234. '</div>';
  235. div = document.createElement("div");
  236. div.id = configElementName;
  237. div.setAttribute('style',
  238. 'visibility: visible;position: fixed;width: 100%;height: 100%;top: 0;left: 0;font-size:12px;' +
  239. 'z-index:1001;text-align:left;');
  240. div.innerHTML = html;
  241. document.body.appendChild(div);
  242. document.getElementById(configElementName + "_close").addEventListener("click", close, false);
  243.  
  244. // Loop through checkboxes to populate them
  245. inputs = div.getElementsByTagName("input");
  246. for (idx = 0; idx < inputs.length; idx++) {
  247. if (inputs[idx].type === "checkbox") {
  248. if (debug) {
  249. window.console.log("Preference for " + inputs[idx].id);
  250. window.console.log(preferences[inputs[idx].id]);
  251. }
  252. if (preferences.hasOwnProperty(inputs[idx].id) && preferences[inputs[idx].id]) {
  253. inputs[idx].setAttribute("checked", "checked");
  254. }
  255. // Add a listener to each checkbox
  256. inputs[idx].addEventListener("click", changeConfiguration, false);
  257. }
  258. }
  259. inputs = div.getElementsByTagName('textarea');
  260. for (idx = 0; idx < inputs.length; idx++) {
  261. // Add a listener to each text area
  262. inputs[idx].addEventListener("change", checkConfiguration, false);
  263. inputs[idx].addEventListener("keyup", checkConfiguration, false);
  264. }
  265.  
  266. // Add event listeners for opening when a click on the head is performed
  267. subFrames = document.getElementsByClassName("h3subframe");
  268. for (idx = 0; idx < subFrames.length; idx++) {
  269. subFrames[idx].addEventListener("click", openSubConfig, false);
  270. }
  271. // Unfold the first one
  272. openSubConfig({
  273. target: document.getElementById('h3subframe1')
  274. });
  275. };
  276. close = function () {
  277. var box = document.getElementById(configElementName);
  278. box.parentNode.removeChild(box);
  279. window.location.reload(false);
  280. };
  281. closeOtherSubConfigs = function (evt) {
  282. var ignore, subFrames, idx;
  283. ignore = evt.target || evt.srcElement;
  284. subFrames = document.getElementsByClassName("h3subframe");
  285. for (idx = 0; idx < subFrames.length; idx++) {
  286. if (ignore !== subFrames[idx]) {
  287. subFrames[idx].nextElementSibling.style.display = "none";
  288. subFrames[idx].addEventListener("click", openSubConfig, false);
  289. }
  290. }
  291. };
  292. closeSubConfig = function (e) {
  293. var evt, target;
  294.  
  295. evt = e || window.event;
  296. target = evt.target || evt.srcElement;
  297. target.removeEventListener("click", closeSubConfig, false);
  298. target.nextElementSibling.style.display = "none";
  299. target.addEventListener("click", openSubConfig, false);
  300. };
  301. openSubConfig = function (e) {
  302. var evt, target;
  303.  
  304. evt = e || window.event;
  305. target = evt.target || evt.srcElement;
  306. target.removeEventListener("click", openSubConfig, false);
  307. target.nextElementSibling.style.display = "block";
  308. closeOtherSubConfigs(evt);
  309. target.addEventListener("click", closeSubConfig, false);
  310. };
  311. changeConfiguration = function (e) {
  312. if (e.target.tagName.toLowerCase() === 'input') {
  313. saveConfiguration(e.target.id, e.target.checked);
  314. }
  315. };
  316. checkConfiguration = function (e) {
  317. var json, idx;
  318.  
  319. if (debug) {
  320. window.console.log('Entering checkConfiguration');
  321. }
  322.  
  323. if (e.target.tagName.toLowerCase() === 'textarea' && e.target.id.indexOf("config_dl_") === 0) {
  324. e.target.classList.remove('valid');
  325. e.target.classList.add('invalid');
  326. try {
  327. json = JSON.parse(e.target.value);
  328. if ((e.target.id === "config_dl_quality" && !isValidQualityConfig(json)) ||
  329. (e.target.id === "config_dl_providers" && !isValidProvidersConfig(json))) {
  330. return;
  331. }
  332. e.target.classList.add('valid');
  333. e.target.classList.remove('invalid');
  334. saveConfiguration(e.target.id.replace("config_", ""), json);
  335. } catch (e) {
  336. if (debug) {
  337. window.console.log('Invalid JSON: '+ e);
  338. }
  339. }
  340. }
  341. };
  342. isValidQualityConfig = function (json) {
  343. if (! Array.isArray(json)) {
  344. if (debug) {
  345. window.console.log('Quality config is no array');
  346. }
  347. return false;
  348. }
  349. return true;
  350. };
  351. isValidProvidersConfig = function (json) {
  352. var idx;
  353. if (! Array.isArray(json)) {
  354. if (debug) {
  355. window.console.log('Quality providers is no array');
  356. }
  357. return false;
  358. }
  359. for (idx = 0; idx < json.length; idx++) {
  360. if (!isValidProviderConfig(json[idx])) {
  361. return false;
  362. }
  363. }
  364. return true;
  365. };
  366. isValidProviderConfig = function (json) {
  367. var idx;
  368.  
  369. if (!json.hasOwnProperty('name') || !json.hasOwnProperty('url') || !json.hasOwnProperty('quality')) {
  370. if (debug) {
  371. window.console.log('Provider config missing name, url or quality property.');
  372. }
  373. return false;
  374. }
  375.  
  376. for (idx = 0; idx < preferences.dl_quality.length; idx++) {
  377. if(!json.quality.hasOwnProperty(preferences.dl_quality[idx])) {
  378. if (debug) {
  379. window.console.log('Provider config missing quality entry.');
  380. }
  381. return false;
  382. }
  383. }
  384.  
  385. if (json.url.indexOf('{show}') === -1 || json.url.indexOf('{season_episode}') === -1 || json.url.indexOf('{quality}') === -1) {
  386. if (debug) {
  387. window.console.log('Provider config url missing {show}, {season_episode} or {quality} section.');
  388. }
  389. return false;
  390. }
  391.  
  392. if (json.hasOwnProperty('invalid_characters')) {
  393. if (! json.invalid_characters.hasOwnProperty('old') || ! json.invalid_characters.hasOwnProperty('new') ||
  394. ! Array.isArray(json.invalid_characters.new) || ! Array.isArray(json.invalid_characters.old) ||
  395. json.invalid_characters.new.length !== json.invalid_characters.old.length
  396. ) {
  397. if (debug) {
  398. window.console.log('Provider config invalid_characters not provided, no array or length not equal between old & new.');
  399. }
  400. return false;
  401. }
  402. }
  403.  
  404. return true;
  405. };
  406. saveConfiguration = function (id, value) {
  407. if (preferences.hasOwnProperty(id)) {
  408. preferences[id] = value;
  409. (async () => {
  410. await GM.setValue(mapping[id], value);
  411. })();
  412. if (debug) {
  413. window.console.log("Stored " + value + " for " + mapping[id]);
  414. }
  415. }
  416. };
  417. loadPreferences = function (callback) {
  418. (async () => {
  419. var key;
  420. if (debug) {
  421. window.console.log("Entering load preferences function");
  422. window.console.log("Preferences (default):");
  423. window.console.log(preferences);
  424. }
  425. for (key in mapping) {
  426. if (mapping.hasOwnProperty(key) && preferences.hasOwnProperty(key)) {
  427. if (debug) {
  428. window.console.log("Retrieving " + key + "...");
  429. }
  430. preferences[key] = await GM.getValue(mapping[key], preferences[key]);
  431. if (debug) {
  432. window.console.log("retrieved " + preferences[key] + " for " + key);
  433. }
  434. }
  435.  
  436. }
  437.  
  438. if (debug) {
  439. window.console.log("Preferences (loaded):");
  440. window.console.log(preferences);
  441. }
  442. callback();
  443. })();
  444. };
  445. getConfigValue = function (name) {
  446. if (preferences.hasOwnProperty(name)) {
  447. return preferences[name];
  448. }
  449. return null;
  450. };
  451. getEnabledSubtitleLanguages = function () {
  452. var result = [];
  453.  
  454. if (preferences.sub_lang_en) {
  455. result.push("en");
  456. }
  457. if (preferences.sub_lang_nl) {
  458. result.push("nl");
  459. }
  460.  
  461. return result;
  462. };
  463. getEnabledSubtitleSources = function () {
  464. var result = [];
  465.  
  466. if (preferences.sub_source_addic7ed) {
  467. result.push(subProviders.sub_source_addic7ed);
  468. }
  469. if (preferences.sub_source_podnapisi) {
  470. result.push(subProviders.sub_source_podnapisi);
  471. }
  472. if (preferences.sub_source_opensubtitles) {
  473. result.push(subProviders.sub_source_opensubtitles);
  474. }
  475. if (preferences.sub_source_subtitleseeker) {
  476. result.push(subProviders.sub_source_subtitleseeker);
  477. }
  478.  
  479. return result;
  480. };
  481. getEnabledDownloadProviders = function () {
  482. return preferences.dl_providers;
  483. };
  484. getEnabledMediaQualities = function () {
  485. return preferences.dl_quality;
  486. };
  487. // Initialize object to return and expose appropriate methods
  488. instance = {};
  489. instance.show = show;
  490. instance.loadPreferences = loadPreferences;
  491. instance.getConfigValue = getConfigValue;
  492. instance.getEnabledSubtitleLanguages = getEnabledSubtitleLanguages;
  493. instance.getEnabledSubtitleSources = getEnabledSubtitleSources;
  494. instance.getEnabledDownloadProviders = getEnabledDownloadProviders;
  495. instance.getEnabledMediaQualities = getEnabledMediaQualities;
  496.  
  497. return instance;
  498. }());
  499. // Initialize variables
  500. debug = true;
  501. // Maps short language keywords to the full English language
  502. languageMap = {
  503. "en": "English",
  504. "nl": "Dutch"
  505. };
  506. // Providers, keys of this MUST be equal to the ones in the configDialog.preferences variable
  507. subProviders = {
  508. sub_source_addic7ed: {
  509. title: "Addic7eD",
  510. createLink: function (showName, showEpisode, language) {
  511. var showNameConverted, showEpisodeConverted, languageConverted;
  512. // Convert show name & show episode to appropriate formats
  513. showNameConverted = this.showConversion(showName);
  514. showEpisodeConverted = this.episodeConversion(showEpisode);
  515. languageConverted = this.languageConversion(language);
  516.  
  517. return "http://www.addic7ed.com/serie/" + showNameConverted + "/" + showEpisodeConverted.season + "/" +
  518. showEpisodeConverted.episode + "/" + languageConverted;
  519. },
  520. showConversion: function (show) {
  521. var exceptions;
  522.  
  523. show = show.replace(/ /g, "_");
  524. // Exception map for shows
  525. exceptions = {
  526. "The_Flash": "The_Flash_(2014)",
  527. "Legends_of_Tomorrow": "DC's_Legends_of_Tomorrow",
  528. "Marvel's_Daredevil": "Daredevil"
  529. };
  530. if (exceptions.hasOwnProperty(show)) {
  531. show = exceptions[show];
  532. }
  533. return show;
  534. },
  535. episodeConversion: function (episode) { return parseEpisode(episode); },
  536. languageConversion: function (language) {
  537. switch (language) {
  538. case "nl":
  539. return "17";
  540. case "en":
  541. return "1";
  542. }
  543. return language;
  544. }
  545. },
  546. sub_source_podnapisi: {
  547. title: "PodNapisi",
  548. createLink: function (showName, showEpisode, language) {
  549. var showNameConverted, showEpisodeConverted, languageConverted;
  550. // Convert show name & show episode to appropriate formats
  551. showNameConverted = this.showConversion(showName);
  552. showEpisodeConverted = this.episodeConversion(showEpisode);
  553. languageConverted = this.languageConversion(language);
  554.  
  555. return "http://www.podnapisi.net/subtitles/search/advanced?keywords=" + showNameConverted + "&seasons="
  556. + showEpisodeConverted.season + "&episodes=" + showEpisodeConverted.episode + "&language=" +
  557. languageConverted;
  558. },
  559. showConversion: function (show) {
  560. var exceptions;
  561.  
  562. show = show.replace(/ /g, "+");
  563. // Exception map for shows
  564. exceptions = {};
  565. if (exceptions.hasOwnProperty(show)) {
  566. show = exceptions[show];
  567. }
  568. return show;
  569. },
  570. episodeConversion: function (episode) { return parseEpisode(episode); },
  571. languageConversion: function (language) { return language; }
  572. },
  573. sub_source_opensubtitles: {
  574. title: "OpenSubtitles",
  575. createLink: function (showName, showEpisode, language) {
  576. var showNameConverted, showEpisodeConverted, languageConverted;
  577. // Convert show name & show episode to appropriate formats
  578. showNameConverted = this.showConversion(showName);
  579. showEpisodeConverted = this.episodeConversion(showEpisode);
  580. languageConverted = this.languageConversion(language);
  581.  
  582. return "http://www.openSubtitles.org/nl/search/searchonlytvseries-on/subformat-srt/sublanguageid-" +
  583. languageConverted + "/season-" + showEpisodeConverted.season + "/episode-" +
  584. showEpisodeConverted.episode + "/moviename-" + showNameConverted;
  585. },
  586. showConversion: function (show) {
  587. var exceptions;
  588.  
  589. show = show.replace(/ /g, "+");
  590. // Exception map for shows
  591. exceptions = {};
  592. if (exceptions.hasOwnProperty(show)) {
  593. show = exceptions[show];
  594. }
  595. return show;
  596. },
  597. episodeConversion: function (episode) { return parseEpisode(episode); },
  598. languageConversion: function (language) {
  599. switch (language) {
  600. case "nl":
  601. return "dut";
  602. case "en":
  603. return "eng";
  604. }
  605. }
  606. },
  607. sub_source_subtitleseeker: {
  608. title: "SubTitleSeeker",
  609. createLink: function (showName, showEpisode, language) {
  610. var showNameConverted, showEpisodeConverted, convertedLanguage;
  611. // Convert show name & show episode to appropriate formats
  612. showNameConverted = this.showConversion(showName);
  613. showEpisodeConverted = this.episodeConversion(showEpisode);
  614. convertedLanguage = this.languageConversion(language);
  615. if (debug) {
  616. window.console.log("Language is not used for subTitleSeeker: " + convertedLanguage);
  617. }
  618.  
  619. return "http://www.subtitleseeker.com/search/TV_EPISODES/" + showNameConverted + "+S" +
  620. showEpisodeConverted.season + "E" + showEpisodeConverted.episode;
  621. },
  622. showConversion: function (show) {
  623. var exceptions;
  624.  
  625. show = show.replace(/ /g, "+");
  626. // Exception map for shows
  627. exceptions = {};
  628. if (exceptions.hasOwnProperty(show)) {
  629. show = exceptions[show];
  630. }
  631. return show;
  632. },
  633. episodeConversion: function (episode) { return parseEpisode(episode); },
  634. languageConversion: function (language) { return language; }
  635. }
  636. };
  637. // Flags
  638. flags = {
  639. "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",
  640. "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=="
  641. };
  642. // Page regexes
  643. pageRegexes = {
  644. // Seriesfeed homepage
  645. start: new RegExp("^/$"),
  646. // Broadcast schedule (format: series/schedule[/{month}]* )
  647. broadcast: new RegExp("^.*/series/schedule(/[a-z]+)*$"),
  648. // Watchlist (format: series/schedule/history[/topshows|/favorieten]*
  649. watch: new RegExp("^.*/series/schedule/history(/[topshows|favorieten]+)*$"),
  650. // Episodes/Seasons (format: series/{name}/episodes[/season/{nr}]* )
  651. season: new RegExp("^.*/series/(.+)/episodes(/season/[0-9]+)*$"),
  652. // Episode (format: series/episode/{nr}/ )
  653. episode: new RegExp("^.*/series/episode/[0-9]+$")
  654. };
  655. // Current page
  656. currentPage = null;
  657.  
  658. // Initialize functions
  659. main = function () {
  660. var head, style;
  661. if (debug) {
  662. window.console.log("Entering main function");
  663. }
  664. // Load preferences
  665. configDialog.loadPreferences(function () {
  666. // Inject config menu item
  667. injectMenuItem();
  668. // Check page we're on
  669. checkPage();
  670. // If the page is still null at this point, we didn't identify the page.
  671. if (currentPage === null) {
  672. window.console.warn("Did not identify a page to run on. Not executing any more page alterations.");
  673. return;
  674. }
  675. // Inject some css
  676. head = document.getElementsByTagName('head')[0];
  677. style = document.createElement('style');
  678. style.setAttribute('type', 'text/css');
  679. style.textContent = '.ui-front { z-index: 1000 !important; }';
  680. head.appendChild(style);
  681. // Modify the page
  682. modifyPage();
  683. });
  684. };
  685. checkPage = function () {
  686. var key, found;
  687. if (debug) {
  688. window.console.log("Entering checkPage function");
  689. }
  690.  
  691. found = null;
  692. for (key in pageRegexes) {
  693. if (pageRegexes.hasOwnProperty(key)) {
  694. if (debug) {
  695. window.console.log("Trying to match " + pageRegexes[key] + " to " + window.location.pathname);
  696. }
  697. if (pageRegexes[key].exec(window.location.pathname)) {
  698. if (debug) {
  699. window.console.log("Match found for " + key);
  700. }
  701. found = key;
  702. break;
  703. }
  704. }
  705. }
  706. currentPage = found;
  707. };
  708. injectMenuItem = function () {
  709. var idx, links, li, menu, inject, injectLink;
  710. if (debug) {
  711. window.console.log("Entering injectMenuItem function");
  712. }
  713. // There are no id's used, so we'll hook on to some text contents in the page
  714. links = document.getElementsByTagName("a");
  715. for (idx = 0; idx < links.length; idx++) {
  716. if (links[idx].innerHTML === "Serie voorstellen") {
  717. // Might have a match, verify "menu" element above
  718. li = links[idx].parentNode;
  719. menu = li.parentNode;
  720. if (menu.classList.contains("main-menu-dropdown")) {
  721. // We can assume safely that we're in a menu. Inject menu item
  722. inject = document.createElement("li");
  723. injectLink = document.createElement("a");
  724. injectLink.style = "cursor: pointer;";
  725. injectLink.innerHTML = "Seriesfeed++ configureren";
  726. injectLink.addEventListener("click", configDialog.show, false);
  727. inject.appendChild(injectLink);
  728. menu.appendChild(inject);
  729. }
  730. }
  731. }
  732. };
  733. modifyPage = function () {
  734. if (debug) {
  735. window.console.log("Entering modifyPage function");
  736. }
  737. // Depending on the type of the page, we need to render differently
  738. switch (currentPage) {
  739. case "start":
  740. handleStartPage();
  741. break;
  742. case "broadcast":
  743. handleBroadcastPage();
  744. break;
  745. case "watch":
  746. handleWatchlistPage();
  747. break;
  748. case "season":
  749. handleSeasonPage();
  750. break;
  751. case "episode":
  752. handleEpisodePage();
  753. break;
  754. default:
  755. window.console.warn("Did not identify a page to run on. Not executing any more page alterations.");
  756. }
  757. // Append css for jquery UI
  758. $("head").append('<link href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css"' +
  759. ' rel="stylesheet" type="text/css">');
  760. };
  761. // Page specific modifications
  762. handleStartPage = function () {
  763. if (debug) {
  764. window.console.log("Entering handleStartPage function");
  765. }
  766. // There is one table of interest: latest favourites. As of 1.3 it can be missing if there's no episodes
  767. injectDefaultTable("favourite_episodes");
  768. };
  769. handleBroadcastPage = function () {
  770. if (debug) {
  771. window.console.log("Entering handleBroadcastPage function");
  772. }
  773. // Single table: broadcasted episodes
  774. injectDefaultTable("afleveringen");
  775. };
  776. handleWatchlistPage = function () {
  777. if (debug) {
  778. window.console.log("Entering handleWatchlistPage function");
  779. }
  780. // Single table: favourites/popular episodes
  781. injectDefaultTable("afleveringen");
  782. };
  783. handleSeasonPage = function () {
  784. var table, showName;
  785. if (debug) {
  786. window.console.log("Entering handleSeasonPage function");
  787. }
  788. // Get show name
  789. showName = document.getElementById('seriesName').value;
  790.  
  791. // Single table: show episodes
  792. table = $("#afleveringen");
  793. // Inject element for header
  794. injectTableHeader("afleveringen");
  795. // Inject icons in rows
  796. table.find("tbody tr[data-aired]").each(function (idx, elm) {
  797. var td, cells, showEpisode;
  798. if (debug) {
  799. window.console.log("Processing row " + idx);
  800. }
  801. td = document.createElement("td");
  802. cells = elm.getElementsByTagName("td");
  803. showEpisode = cells[0].firstElementChild.innerHTML;
  804. td.appendChild(createFunctionality(showName, showEpisode));
  805. elm.appendChild(td);
  806. });
  807. };
  808. handleEpisodePage = function () {
  809. var table, row, cell, data, showName, showEpisode, banner, episodeTitle;
  810.  
  811. if (debug) {
  812. window.console.log("Entering handleEpisodePage function");
  813. }
  814. // Need to inject new row instead of cell
  815. banner = $('.showBanner').find('img');
  816. table = $("#episodeInfo");
  817. episodeTitle = table.siblings('h3').html();
  818. data = episodeTitle.match(/(.*) - (.*)/);
  819. showName = banner.attr('title').replace('Banner voor ','');
  820. showEpisode = data[0];
  821. // Inject
  822. row = document.createElement('tr');
  823. cell = document.createElement('td');
  824. cell.innerHTML = 'Seriesfeed++';
  825. row.appendChild(cell);
  826. cell = document.createElement('td');
  827. cell.appendChild(createFunctionality(showName, showEpisode));
  828. row.appendChild(cell);
  829. table.find("tbody").append(row);
  830. };
  831. // General modification methods
  832. injectTableHeader = function (tableId) {
  833. var table, th;
  834. if (debug) {
  835. window.console.log("Entering injectTableHeader function");
  836. }
  837.  
  838. table = $("#" + tableId);
  839. // Inject element for header
  840. th = document.createElement("th");
  841. th.innerHTML = "Seriesfeed++";
  842. table.find("thead tr")[0].appendChild(th);
  843. };
  844. injectDefaultTable = function (tableId) {
  845. var table, colspan, readMore;
  846. if (debug) {
  847. window.console.log("Entering injectDefaultTable function");
  848. }
  849.  
  850. table = $("#" + tableId);
  851. // Check if element actually exists
  852. if (table.length === 0) {
  853. if (debug) {
  854. window.console.log("Did not find a table with the id: " + tableId);
  855. }
  856. return;
  857. }
  858. // Inject element for header
  859. injectTableHeader(tableId);
  860. // Inject icons in rows
  861. table.find("tbody tr").not('.readMore').each(function (idx, elm) {
  862. var td, cells, showName, showEpisode;
  863. if (debug) {
  864. window.console.log("Processing row" + idx);
  865. }
  866. if ($(elm).attr('data-aired') === undefined) {
  867. if (debug) {
  868. window.console.log("Skipping row because it has no data-aired attribute");
  869. }
  870. return;
  871. }
  872. td = document.createElement("td");
  873. cells = elm.getElementsByTagName("td");
  874. showName = cells[0].firstElementChild.innerHTML;
  875. showEpisode = cells[1].firstElementChild.innerHTML;
  876. td.appendChild(createFunctionality(showName, showEpisode));
  877. elm.appendChild(td);
  878. });
  879. readMore = table.find("tbody tr.readMore td");
  880. colspan = parseInt(readMore.attr("colspan"), 10);
  881. readMore.attr('colspan', colspan + 1);
  882. };
  883. createFunctionality = function (showName, showEpisode) {
  884. var span, languages, idx, downloadProviders, downloadTypes, downloadIcon;
  885. if(debug){
  886. window.console.log(
  887. "Entering createFunctionality with parameters: showName: "+showName+", showEpisode: "+showEpisode);
  888. }
  889.  
  890. span = document.createElement("span");
  891. // Add language flags
  892. languages = configDialog.getEnabledSubtitleLanguages();
  893. for (idx = 0; idx < languages.length; idx++) {
  894. span.appendChild(createLanguageFlag(languages[idx], showName, showEpisode));
  895. span.appendChild(document.createTextNode(" "));
  896. }
  897. downloadProviders = configDialog.getEnabledDownloadProviders();
  898. downloadTypes = configDialog.getEnabledMediaQualities();
  899. if (downloadProviders.length > 0 && downloadTypes.length > 0) {
  900. downloadIcon = document.createElement("i");
  901. downloadIcon.setAttribute('class','fa fa-download');
  902. downloadIcon.setAttribute('style', 'display:inline-block; font-size: 19px; cursor: pointer;');
  903. downloadIcon.title = "download episode";
  904. downloadIcon.addEventListener("click", showDlSelectionDialog);
  905.  
  906. span.appendChild(downloadIcon);
  907. }
  908. return span;
  909. };
  910. createLanguageFlag = function (lang, showName, showEpisode) {
  911. var result, img, subSources;
  912. if(debug){
  913. window.console.log(
  914. "Entering createLanguageFlag with parameters: lang: " + lang + ", showName: " + showName +
  915. ", showEpisode: " + showEpisode);
  916. }
  917.  
  918. if (!flags.hasOwnProperty(lang)) {
  919. throw new Error(lang + "is not a recognized language flag!");
  920. }
  921.  
  922. img = document.createElement("img");
  923. img.src = flags[lang];
  924. img.alt = lang + " flag";
  925. img.title = languageMap[lang] + " subtitles";
  926. img.setAttribute("data-language", lang);
  927. img.setAttribute('style', 'height: 16px; vertical-align:top; cursor: pointer;');
  928. // If there's just one subtitle source, make it a link, otherwise make it a pop-up menu
  929. subSources = configDialog.getEnabledSubtitleSources();
  930. if (subSources.length > 1) {
  931. img.addEventListener("click", showSubSelectionDialog, false);
  932. result = img;
  933. } else {
  934. result = document.createElement("a");
  935. result.href = subSources[0].createLink(showName, showEpisode, lang);
  936. result.target = "_blank";
  937. result.appendChild(img);
  938. }
  939.  
  940. return result;
  941. };
  942. showSubSelectionDialog = function (e) {
  943. var evt, target, dialog, subSources, row, showName, showEpisode, lang, cells, idx, link, p, thead, data;
  944.  
  945. if (debug) {
  946. window.console.log("Entering showSubSelectionDialog - currentPage: " + currentPage);
  947. }
  948.  
  949. evt = e || window.event;
  950. target = evt.target || evt.srcElement;
  951.  
  952. // Get language
  953. lang = target.getAttribute("data-language");
  954. // Get row, so we can extract show name & episode
  955. row = target.parentNode.parentNode.parentNode;
  956. cells = row.getElementsByTagName("td");
  957. if(currentPage === "season"){
  958. showName = document.getElementById('seriesName').value;
  959. showEpisode = cells[0].firstElementChild.innerHTML;
  960. } else if(currentPage === "episode") {
  961. showName = document.getElementById('seriesName').value;
  962. showEpisode = $("#episodeInfo").prev("h3").html().trim();
  963. } else {
  964. showName = cells[0].firstElementChild.innerHTML;
  965. showEpisode = cells[1].firstElementChild.innerHTML;
  966. }
  967. if (debug) {
  968. window.console.log("Retrieved next name & episode: " + showName + " - " + showEpisode);
  969. }
  970.  
  971. // Build dialog
  972. dialog = document.createElement("div");
  973. p = document.createElement("p");
  974. p.innerHTML = "Show: " + showName + "<br/>Episode: " + showEpisode;
  975. dialog.appendChild(p);
  976. p = document.createElement("p");
  977. // Get sub source sites
  978. subSources = configDialog.getEnabledSubtitleSources();
  979. for (idx = 0; idx < subSources.length; idx++) {
  980. link = document.createElement("a");
  981. link.target = "_blank";
  982. link.href = subSources[idx].createLink(showName, showEpisode, lang);
  983. link.innerHTML = subSources[idx].title;
  984. link.setAttribute("style","text-decoration: underline;");
  985. link.addEventListener("click", function () {
  986. $(dialog).dialog("close");
  987. }, false);
  988. p.appendChild(link);
  989. p.appendChild(document.createTextNode(" "));
  990. }
  991. dialog.appendChild(p);
  992. $(dialog).dialog({
  993. title: "Download " + languageMap[lang] + " subtitles",
  994. position: { my: "right bottom", at: "top left", of: target }
  995. });
  996. };
  997. showDlSelectionDialog = function (e) {
  998. var evt, target, dialog, row, cells, showName, showEpisode, p, mediaQuality, mediaProviders, idx, jdx,
  999. table_head, data, providers, banner, episodeTitle, table;
  1000.  
  1001. evt = e || window.event;
  1002. target = evt.target || evt.srcElement;
  1003.  
  1004. // Get row, so we can extract show name & episode
  1005. row = target.parentNode.parentNode.parentNode;
  1006. cells = row.getElementsByTagName("td");
  1007. if(currentPage === "season") {
  1008. showName = document.getElementById('seriesName').value;
  1009. showEpisode = cells[0].firstElementChild.innerHTML;
  1010. } else if(currentPage === "episode") {
  1011. banner = $('.showBanner').find('img');
  1012. table = $("#episodeInfo");
  1013. episodeTitle = table.siblings('h3').html();
  1014. data = episodeTitle.match(/(.*) - (.*)/);
  1015. showName = banner.attr('title').replace('Banner voor ','');
  1016. showEpisode = data[0];
  1017. } else {
  1018. showName = cells[0].firstElementChild.innerHTML;
  1019. showEpisode = cells[1].firstElementChild.innerHTML;
  1020. }
  1021.  
  1022. dialog = document.createElement("div");
  1023. p = document.createElement("p");
  1024. p.innerHTML = "Show: " + showName + "<br/>Episode: " + showEpisode;
  1025. dialog.appendChild(p);
  1026. p = document.createElement("p");
  1027. // Get types & sites
  1028. mediaProviders = configDialog.getEnabledDownloadProviders();
  1029. mediaQuality = configDialog.getEnabledMediaQualities();
  1030. providers = mediaProviders.length;
  1031. for (idx = 0; idx < mediaQuality.length; idx++) {
  1032. p = document.createElement("p");
  1033. p.appendChild(document.createTextNode(mediaQuality[idx]));
  1034. dialog.appendChild(p);
  1035. p = document.createElement("p");
  1036. for (jdx = 0; jdx < providers; jdx++) {
  1037. if(mediaProviders[jdx].quality.hasOwnProperty(mediaQuality[idx])) {
  1038. p.appendChild(createMediaLink(mediaProviders[jdx], showName, showEpisode, mediaQuality[idx], dialog));
  1039. if(jdx < providers - 1) {
  1040. p.appendChild(document.createTextNode(", "));
  1041. }
  1042. }
  1043. }
  1044. dialog.appendChild(p);
  1045. }
  1046.  
  1047. $(dialog).dialog({
  1048. title: "Download episode",
  1049. position: { my: "right bottom", at: "top left", of: target }
  1050. });
  1051. };
  1052. // Helper functions
  1053. parseEpisode = function (showEpisode) {
  1054. var result, regex, match;
  1055. if (debug) {
  1056. window.console.log("Entering parseEpisode function");
  1057. }
  1058.  
  1059. result = {
  1060. season: 0,
  1061. episode: 0,
  1062. title: ""
  1063. };
  1064. regex = new RegExp("S([0-9]+)E([0-9]+) - (.+)");
  1065. // Epected format: SxEy - episode title
  1066. match = regex.exec(showEpisode);
  1067. if (match !== null) {
  1068. result.season = parseInt(match[1], 10);
  1069. result.episode = parseInt(match[2], 10);
  1070. result.title = match[3];
  1071. } else {
  1072. window.console.warn("Could not parse " + showEpisode + " correctly!");
  1073. }
  1074. return result;
  1075. };
  1076. createMediaLink = function (mediaProviderConfig, showName, showEpisode, mediaType, dialog) {
  1077. var a, idx, episodeData, closeDialog, quality;
  1078.  
  1079. quality = mediaProviderConfig.quality[mediaType];
  1080. if (mediaProviderConfig.hasOwnProperty('invalid_characters')) {
  1081. for (idx = 0; idx < mediaProviderConfig.invalid_characters.old.length; idx++) {
  1082. showName = showName.replace(
  1083. mediaProviderConfig.invalid_characters.old[idx],
  1084. mediaProviderConfig.invalid_characters.new[idx]
  1085. );
  1086. quality = quality.replace(
  1087. mediaProviderConfig.invalid_characters.old[idx],
  1088. mediaProviderConfig.invalid_characters.new[idx]
  1089. );
  1090. }
  1091. }
  1092. episodeData = parseEpisode(showEpisode);
  1093. if (mediaProviderConfig.hasOwnProperty('episodeCharacter')){
  1094. showEpisode = formatToConvention(episodeData, mediaProviderConfig.episodeCharacter);
  1095. } else {
  1096. showEpisode = formatToConvention(episodeData);
  1097. }
  1098.  
  1099.  
  1100. closeDialog = function () {
  1101. $(dialog).dialog("close");
  1102. };
  1103. a = document.createElement("a");
  1104. a.href = mediaProviderConfig.url.replace('{show}', encodeURIComponent(showName)).replace('{season_episode}', encodeURIComponent(showEpisode)).replace('{quality}', encodeURIComponent(quality));
  1105. a.target = "_blank";
  1106. a.innerHTML = mediaProviderConfig.name;
  1107. a.setAttribute("style","text-decoration: underline;");
  1108. a.addEventListener("mouseup", closeDialog, false);
  1109.  
  1110. return a;
  1111. };
  1112. formatToConvention = function (episodeData, episodeCharacter) {
  1113. episodeCharacter = episodeCharacter || "E";
  1114. return "S" + ((episodeData.season < 10) ? "0" : "") + episodeData.season + episodeCharacter +
  1115. ((episodeData.episode < 10) ? "0" : "") + episodeData.episode;
  1116. };
  1117.  
  1118. // Expose methods to the outside world
  1119. seriesFeedPlusPlus.main = main;
  1120.  
  1121. return seriesFeedPlusPlus;
  1122. }());
  1123.  
  1124. // Execute main
  1125. try {
  1126. seriesFeedPlusPlus.main();
  1127. } catch (e) {
  1128. console.log(e);
  1129. console.log(e.stack);
  1130. // Display error
  1131. var txt = "An error occurred while executing this script.\n\n";
  1132. txt += "Issue: <<<" + e.message + ">>>\n\n";
  1133. txt += "\nPlease report this back to the author (on the greasyfork website, or by sending me an email at mrinvisible@cryptolab.net) so it can be corrected.\n\n";
  1134. txt += "Click 'OK' to continue.\n\n";
  1135. window.alert(txt);
  1136. }