// ==UserScript== // @name dimsum.my // @namespace https://github.com/ngsoft // @version 6.3.1 // @description Subtitle downloader // @author daedelus // @include /^https?://(www.)?dimsum.my// // @run-at document-end // @noframes // @grant none // @updateURL https://raw.githubusercontent.com/ngsoft/archives/master/dimsum.my.user.js // @downloadURL https://raw.githubusercontent.com/ngsoft/archives/master/dimsum.my.user.js // @compatible firefox+greasemonkey(3.17) // @compatible firefox+tampermonkey // @compatible chrome+tampermonkey // @icon https://edm.dimsum.my/favicon.ico // ==/UserScript== (function(doc, win, undef) { let GMinfo = (GM_info ? GM_info : (typeof GM === 'object' && GM !== null && typeof GM.info === 'object' ? GM.info : null)); let scriptname = `${GMinfo.script.name} version ${GMinfo.script.version}`; function html2element(html) { if (typeof html === "string") { let template = doc.createElement('template'); html = html.trim(); template.innerHTML = html; return template.content.firstChild; } } function addcss(css) { if (typeof css === "string" && css.length > 0) { let s = doc.createElement('style'); s.setAttribute('type', "text/css"); s.appendChild(doc.createTextNode('')); doc.head.appendChild(s); } } /** * Uses Mutation Observer to find Nodes by selector when created or available * @type {function} * @param {Element|Document} el Root Element * @param {string} selector A valid selector * @param {function} callback If callback returns false, stops the observer * @param {boolean} [once] Stops the observer when finding first node, defaults to true */ const findNode = (() => { const MutationObserver = win.MutationObserver || win.WebKitMutationObserver || win.MozMutationObserver; const s = "string", b = "boolean", f = "function", o = "object", u = "undefined", n = "number"; const options = { attributes: true, characterData: true, childList: true, subtree: true }; const defaults = { selector: "", callback: null, once: true, uid: null }; let uid = 0; function triggerEvent(node, params, obs, el) { let event = new Event("DOMNodeFound", {bubbles: true, cancelable: true}); event.data = { options: params, observer: obs, current: el }; node.dispatchEvent(event); } function nodeFinder(el) { let params = Object.assign({}, defaults); for (let i = 1; i < arguments.length; i++) { let arg = arguments[i]; switch (typeof arg) { case s: params.selector = arg; break; case b: params.once = arg; break; case f: params.callback = arg; break; case o: if (isPlainObject(arg)) { Object.assign(params, arg); } break; } } if (typeof params.callback === f) { params.uid = uid++; let matches = []; const run = function run() { el.querySelectorAll(params.selector).forEach((target) => { if (!matches.includes(target)) { matches.push(target); triggerEvent(target, params, observer, el); } }); }; const DOMNodeFound = function DOMNodeFound(e) { //no multi triggers for other searches if (e.data === undef || DOMNodeFound.uid !== e.data.options.uid) { return; } let self = e.target, obs = e.data.observer; if (self.matches(params.selector)) { let retval = params.callback.call(self, e); if (params.once === true || retval === false) { el.removeEventListener(e.type, DOMNodeFound); obs.disconnect(); } return retval; } }; DOMNodeFound.uid = params.uid; el.addEventListener("DOMNodeFound", DOMNodeFound, false); let observer = new MutationObserver(function(m, obs) { for (let i = 0; i < m.length; i++) { let rec = m[i], target = rec.target; if (target.matches === undef) { continue; } if (typeof target.closest === "function" && target.closest(params.selector) !== null) { run(); } } }); observer.observe(el, options); if (doc.readyState === 'loading') { doc.addEventListener('DOMContentLoaded', function DOMContentLoaded() { doc.removeEventListener('DOMContentLoaded', DOMContentLoaded); run(); }); } if (doc.readyState !== 'complete') { win.addEventListener('load', function load() { win.removeEventListener('load', load); run(); }); return; } run(); } } return nodeFinder; })(); const notifications = (() => { const styles = ` /* Animations */ @keyframes fadeInRight {0% {opacity: 0;-webkit-transform: translate3d(100%, 0, 0);transform: translate3d(100%, 0, 0);}100% {opacity: 1;-webkit-transform: none;transform: none;}} @keyframes bounceOut {20% {-webkit-transform: scale3d(.9, .9, .9);transform: scale3d(.9, .9, .9);}50%, 55% {opacity: 1;-webkit-transform: scale3d(1.1, 1.1, 1.1);transform: scale3d(1.1, 1.1, 1.1);}100% {opacity: 0;-webkit-transform: scale3d(.3, .3, .3);transform: scale3d(.3, .3, .3);}} .bounceOut {animation-name: bounceOut;animation-duration: .75s;animation-duration: 1s;animation-fill-mode: both;} .fadeIn {animation-name: fadeInRight;animation-duration: .5s;animation-fill-mode: both;} /* Position & Size */ .user-notifications{position: absolute;right: 64px; left: auto; bottom: 27.5%;text-align: right;font-size: 16px;z-index: 9999; min-width: 256px;} body > .user-notifications{position: fixed;} .user-notify{display: block; text-align:center;padding:16px 24px; border-radius: 4px; margin: 8px 0;} .user-notify [class*="-icon"]{width: 32px;height: 32px;margin:-4px 8px 0 -8px; float:left;} /* Colors & Fonts */ .user-notifications{font-family: Arial,Helvetica,sans-serif;font-size:16px;} .user-notify{color:rgba(52, 58, 64, 1);background-color: rgba(248, 249, 250, .8); border: 1px solid rgba(34, 34, 34, 1);} .user-notify .error-icon{color: rgba(220, 53, 69, .8);}.user-notify .success-icon{color: rgba(40, 167, 69, .8);}`; const template = { notify: `
`, success: `
`, error: `
` }; const eventend = ((div) => { const browerevents = { animation: 'animationend', OAnimation: 'oAnimationEnd', MozAnimation: 'mozAnimationEnd', WebkitAnimation: 'webkitAnimationEnd' }; for (let style in browerevents) { if (div.style[style] !== "undefined") { return browerevents[style]; } } })(doc.createElement('div')); const eventstart = eventend.replace(/End$/, 'Start').replace(/end$/, 'start'); const defaults = { timeout: 1, callback: null }; function notify(el, message, ...extra) { const params = Object.assign({}, defaults); extra.forEach((arg) => { if (typeof arg === "number") { params.timeout = arg > 1000 ? (arg / 1000) : arg; } else if (typeof arg === "function") { params.callback = arg; } }); if (typeof params.callback === "function") { message.addEventListener("notifyend", params.callback); } message.addEventListener("click", function() { message.addEventListener(eventstart, function() { message.addEventListener(eventend, function() { message.dispatchEvent(new Event('notifyend', {bubbles: true, cancelable: true})); message.parentElement.removeChild(message); }, {once: true}); }, {once: true}); this.classList.add('bounceOut'); }, {once: true}); message.addEventListener(eventstart, function() { message.addEventListener(eventend, function() { this.classList.remove('fadeIn'); if (params.timeout === 0) return; setTimeout(() => { message.dispatchEvent(new Event('click', {bubbles: true, cancelable: true})); }, (params.timeout * 1000)); }, {once: true}); }, {once: true}); el.insertBefore(message, el.firstChild); //el.appendChild(message); message.classList.add('fadeIn'); } function notifications(el) { if (!(this instanceof notifications) || typeof el.appendChild !== "function") return; if (notifications.styles === false) { addcss(styles); notifications.styles = true; } const root = this.root = html2element(`
`); el.appendChild(root); } notifications.styles = false; notifications.prototype = { /** * Display a notification * @param {string} message Message to display * @param {number} [timeout] timeout in seconds for the notification to disappear (defaults to 1s + animations) * @param {function} [callback] callback to call * @returns {this} */ notify(message, timeout, callback) { if (typeof message === "string") { message = doc.createTextNode(message); let el = html2element(template.notify); el.appendChild(message); notify(this.root, el, timeout, callback); } return this; }, /** * Display an error * @param {string} message Message to display * @param {number} [timeout] timeout in seconds for the notification to disappear (defaults to 1s + animations) * @param {function} [callback] callback to call * @returns {this} */ error(message, timeout, callback) { if (typeof message === "string") { message = doc.createTextNode(message); let el = html2element(template.error); el.appendChild(message); notify(this.root, el, timeout, callback); } return this; }, /** * Display a success notification * @param {string} message Message to display * @param {number} [timeout] timeout in seconds for the notification to disappear (defaults to 1s + animations) * @param {function} [callback] callback to call * @returns {this} */ success(message, timeout, callback) { if (typeof message === "string") { message = doc.createTextNode(message); let el = html2element(template.success); el.appendChild(message); notify(this.root, el, timeout, callback); } return this; } }; return notifications; })(); const notify = new notifications(doc.body); let styles = ` .jw-settings-content-item{width: 80% !important;} .download-button {text-decoration: none;width:32px;height:31px;line-height:0;vertical-align: middle;display: inline-block;float: right;padding: 4px;} .download-button > * {width:100%;height:100%;} .download-button{color: #fff!important;border-radius: 3px;border: 1px solid rgba(0,0,0,0);} .download-button:hover{border-color: #ec1c24; background: #ec1c24;} .player-fullscreen{z-index: 9000;} button.jw-settings-content-item + .download-button{width:24px;height:23px;} .hidden, .hidden *, [id*="hola_"] { position: fixed !important; right: auto !important; bottom: auto !important; top:-100% !important; left: -100% !important; height: 1px !important; width: 1px !important; opacity: 0 !important;max-height: 1px !important; max-width: 1px !important; display: inline !important;z-index: -1 !important; } `; addcss(styles); function downloadButton(subtrack) { if (!(this instanceof downloadButton)) { return; } if (!(subtrack instanceof Element) || typeof subtrack.matches !== "function" || !subtrack.matches('[data-id]') || typeof playerModule === "undefined" || !Array.isArray(playerModuleJw.subtitles)) return; this.subtrack = subtrack; Object.assign(this, { __title: null, __button: null, __subinfo: null, xhr: null, subtrack: subtrack }); return this.button; } downloadButton.prototype = { template: ``, extension: "srt", timeout: 10000, get button() { if(this.__button === null){ let btn = this.__button = html2element(this.template); btn.title = `< Download ${this.lang} Subtitle. >`; btn.download = this.filename; btn.href = this.src; this.subtrack.parentElement.insertBefore(btn, this.subtrack.nextSibling); Object.defineProperty(btn, 'bdata', { configurable: true, value: this }); btn.addEventListener('click', this.click); } return this.__button; }, get id() { return parseInt(this.subtrack.dataset.id); }, get title() { //playerModuleJw if (this.__title === null) { let type = playerModuleJw.engageData.type, baseTitle = playerModuleJw.engageData.title, matches, title = "", season, episode; switch (type) { case "movie": title = baseTitle.replace(/[\|&;\$%@"\'’<>\(\)\+,]/g, "."); break; case "episode": if ((matches = playerModuleJw._title.match(/E([0-9]+)(?:\s+)?(.*?)$/i)) !== null) { title = matches[2].replace(/[\|&;\$%@"\'’<>\(\)\+,]/g, ".").replace(/S[0-9]+$/, "").trim() + "."; season = parseInt(playerModuleJw.gtmData.seasonNumber); episode = parseInt(matches[1]); if (season > 1) { title += "S" + (season > 9 ? season : "0" + season); } title += "E" + (episode > 9 ? episode : "0" + episode); } break; } if (title.length > 0) this.__title = title; } return this.__title; }, get lang() { return this.subtrack.innerText.trim(); }, get langcode() { return this.subinfo.srclang; }, get src() { return this.subinfo.src; }, get subinfo() { if (this.__subinfo === null) { let self = this; this.__subinfo = Object.assign({}, playerModuleJw.subtitles.filter(x => x.id === self.id)[0]); } return this.__subinfo; }, get filename() { return `${this.title}.${this.langcode}.${this.extension}`; }, click(e) { e.preventDefault(); const self = this.bdata; if (self !== undef) { if (self.xhr === null || (self.xhr.readyState === 4 && self.xhr.status !== 200)) { let xhr = self.xhr = new XMLHttpRequest(); xhr.open("GET", self.src, true); xhr.timeout = self.timeout; xhr.onerror = xhr.ontimeout = xhr.onabort = function onerror() { self.onerror.call(self, self); }; xhr.onload = function onload() { if (xhr.readyState === 4 && xhr.status === 200 && xhr.responseText.length > 0) { return self.onload.call(self, self); } self.onerror.call(self, self); }; } else if (self.xhr.readyState > 0) { if(self.xhr.responseText.length > 0 && self.xhr.status === 200){ return self.onload.call(self, self); } return self.onerror.call(self, self); } self.xhr.send(); } }, onload(self) { let txt = self.xhr.responseText; let blob = new Blob([txt], {type: 'octet/stream'}), link = doc.createElement('a'), href = URL.createObjectURL(blob); link.download = self.filename; link.href = href; link.style.opacity = 0; doc.body.appendChild(link); link.click(); doc.body.removeChild(link); setTimeout(x => URL.revokeObjectURL(href), 2000); }, onerror(self) { notify.error(`Cannot download ${self.filename}`); } }; /** * Checks if el has closest element matching the selector * @param {EventTarget} el * @param {stringe} selector * @returns {EventTarget|null} */ function matches(el, selector) { if (typeof el.closest === "function" && typeof selector === "string") return el.closest(selector); return null; } findNode(doc.body, '#playerModule__player .jw-settings-menu', function() { if (this.querySelector('.download-button') !== null) return; if(!Array.isArray(playerModuleJw.subtitles)) return; let subindex = 0, ccbtn = doc.querySelector('.jw-icon-cc.jw-settings-submenu-button'); this.querySelectorAll('.jw-settings-submenu').forEach(function(jwsettings, n) { if (n > 0) return; //disables: autoplay doc.querySelector('video').addEventListener('loadeddata', function() { this.style['object-fit'] = "fill"; this.pause(); }); jwsettings.querySelectorAll('button.jw-settings-content-item').forEach(function(el, index) { if (index === 0) return; if (/^([0-9]+)p/.test(el.innerHTML)) return; el.dataset.id = playerModuleJw.subtitles[subindex].id; let button = new downloadButton(el); subindex++; if (button.bdata.langcode === 'en') { if(ccbtn !== null){ let clone = button.cloneNode(true); ccbtn.parentElement.insertBefore(clone, ccbtn); clone.addEventListener("click", function(e) { const self = this; e.preventDefault(); doc.querySelectorAll(`.download-button[href="${this.href}"]`).forEach(function(el) { if (el === self) return; el.click(); }); }); } notify.success(`English subtitles are available.`, 2.5); } }); }); }, false); /** * Disable trial web adds */ let ad_interval = setInterval(function() { if (typeof dfp_config !== "undefined") { clearInterval(ad_interval); //dfp_config.ads_for_entire_site = false; Object.keys(dfp_config.ads_enable).forEach(function(type) { dfp_config.ads_enable[type] = false; }); } }, 20); console.debug(scriptname, 'Started'); })(document, window);