// ==UserScript== // @name MylistPocket // @namespace https://github.com/segabito/ // @description 動画を「あとで見る」ツール。 ZenzaWatchとの連携も可能 // @match http://www.nicovideo.jp/* // @match http://ext.nicovideo.jp/ // @match http://ext.nicovideo.jp/#* // @match http://ch.nicovideo.jp/* // @match http://com.nicovideo.jp/* // @match http://commons.nicovideo.jp/* // @match http://dic.nicovideo.jp/* // @match http://ex.nicovideo.jp/* // @match http://info.nicovideo.jp/* // @match http://search.nicovideo.jp/* // @match http://uad.nicovideo.jp/* // @exclude http://ads*.nicovideo.jp/* // @exclude http://www.upload.nicovideo.jp/* // @exclude http://www.nicovideo.jp/watch/*?edit=* // @exclude http://ch.nicovideo.jp/tool/* // @exclude http://flapi.nicovideo.jp/* // @exclude http://dic.nicovideo.jp/p/* // @version 0.0.2 // @grant none // @author segabito macmoto // @license public domain // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js // @downloadURL none // ==/UserScript== (function() { const PRODUCT = 'MylistPocket'; const monkey = function() { const console = window.console; console.log('exec MylistPocket..'); const $ = window.mpJQuery || window.jQuery, _ = window._; const TOKEN = 'r:' + (Math.random()); const PRODUCT = 'MylistPocket'; const CONSTANT = { BASE_Z_INDEX: 100000 }; const MylistPocket = {debug: {}}; window.MylistPocket = MylistPocket; const __css__ = (` a[href*='watch/'] { display: inline-block; } .mylistPocketHoverMenu { display: none; opacity: 0.8; position: absolute; z-index: ${CONSTANT.BASE_Z_INDEX + 100000}; font-size: 8pt; padding: 0; line-height: 26px; font-weight: bold; text-align: center; transition: box-shadow 0.2s ease, opacity 0.4s ease, padding 0.2s ease; user-select: none; -webkit-user-select: none; -moz-user-select: none; } .mylistPocketHoverMenu.is-busy { opacity: 0 !important; pointer-events: none; } .mylistPocketHoverMenu.is-otherDomain .wwwOnly { display: none; } .mylistPocketHoverMenu.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly { display: none; } .mylistPocketButton { /*font-family: Menlo;*/ display: block; font-weight: bolder; cursor: pointer; width: 32px; height: 26px; cursor: pointer; box-shadow: 1px 1px 1px #000; transition: 0.1s box-shadow ease, 0.1s transform ease; font-size: 16px; line-height: 24px; -webkit-user-select: none; -moz-use-select: none; user-select: none; outline: none; } .mylistPocketButton:hover { transform: scale(1.2); box-shadow: 4px 4px 5px #000; } .mylistPocketButton:active { transform: scale(1.0); box-shadow: none; transition: none; } .is-deflistUpdating .mylistPocketButton.deflist-add::after, .is-deflistSuccess .mylistPocketButton.deflist-add::after, .is-deflistFail .mylistPocketButton.deflist-add::after, .mylistPocketButton:hover::after, #mylistPocket-poupup [tooltip] { content: attr(tooltip); position: absolute; /*top: 0px; left: 50%;*/ top: 50%; right: -8px; padding: 2px 4px; white-space: nowrap; font-size: 12px; color: #fff; background: #333; transform: translate3d(-50%, -120%, 0); transform: translate3d(100%, -50%, 0); pointer-events: none; } .is-deflistUpdating .mylistPocketButton.deflist-add { cursor: wait; opacity: 0.9; transform: scale(1.0); box-shadow: none; transition: none; background: #888; border-style: inset; } .is-deflistSuccess .mylistPocketButton.deflist-add, .is-deflistFail .mylistPocketButton.deflist-add { transform: scale(1.0); box-shadow: none; transition: none; } .is-deflistSuccess .mylistPocketButton.deflist-add::after { content: attr(data-result); background: #393; } .is-deflistFail .mylistPocketButton.deflist-add::after { content: attr(data-result); background: #933; } .is-deflistUpdating .mylistPocketButton.deflist-add::after { content: '更新中'; background: #333; } .mylistPocketButton + .mylistPocketButton { margin-top: 4px; } .mylistPocketHoverMenu:hover { font-weibht: bolder; opacity: 1; } .mylistPocketHoverMenu:active { } .mylistPocketHoverMenu.is-show { display: block; } #mylistPocket-popup .owner-icon { width: 64px; height: 64px; transform-origin: center; transform-origin: center; transition: 0.2s transform ease, 0.2s box-shadow ease ; } #mylistPocket-popup .owner-icon:hover { } #mylistPocket-popup .description a { color: #ffff00 !important; text-decoration: none !important; font-weight: normal !important; display: inline-block; } #mylistPocket-popup .description a.watch { display: block; } #mylistPocket-popup .description a:visited { color: #ffff99 !important; } #mylistPocket-popup .description button { /*font-family: Menlo;*/ font-size: 16px; font-weight: bolder; margin: 4px 8px; padding: 4px 8px; cursor: pointer; border-radius: 0; background: #333; color: #ccc; border: solid 2px #ccc; outline: none; } #mylistPocket-popup .description button:hover { transform: translate(-2px,-2px); box-shadow: 2px 2px 2px #000; background: #666; transition: 0.2s transform ease, 0.2s box-shadow ease ; } #mylistPocket-popup .description button:active { transform: none; box-shadow: none; transition: none; } #mylistPocket-popup .description button:active::hover { opacity: 0; } #mylistPocket-popup .watch { display: block; position: relative; line-height: 60px; box-sizing: border-box; padding: 4px 16px;; min-height: 60px; width: 280px; margin: 8px 10px; background: #444; border-radius: 4px; } #mylistPocket-popup .watch:hover { background: #446; } #mylistPocket-popup .videoThumbnail { position: absolute; right: 16px; height: 60px; transform-origin: center; transition: 0.2s transform ease, 0.2s box-shadow ease ; } #mylistPocket-popup .videoThumbnail:hover { transform: scale(2); box-shadow: 0 0 8px #888; transition: 0.2s transform ease 0.5s, 0.2s box-shadow ease 0.5s ; } `).trim(); const __tpl__ = (`
【実況】どんぐりころころの大冒険 Part1(最終回) 1970/01/01 00:00 12,345 6,789 2,525 1:23 1234 ほげほげ
`).trim(); // TODO: ライブラリ化 const util = MylistPocket.util = (() => { const util = {}; util.addStyle = function(styles, id) { var elm = document.createElement('style'); elm.type = 'text/css'; if (id) { elm.id = id; } var text = styles.toString(); text = document.createTextNode(text); elm.appendChild(text); var head = document.getElementsByTagName('head'); head = head[0]; head.appendChild(elm); return elm; }; util.mixin = function(self, o) { _.each(Object.keys(o), f => { if (!_.isFunction(o[f])) { return; } if (_.isFunction(self[f])) { return; } self[f] = o[f].bind(o); }); }; util.createWebWorker = function(func) { const src = func.toString().replace(/^function.*?\{/, '').replace(/}$/, ''); const blob = new Blob([src], {type: 'text\/javascript'}); const url = URL.createObjectURL(blob); return new Worker(url); }; util.attachShadowDom = function({host, tpl, mode = 'open'}) { const root = host.attachShadow({mode}); const node = document.importNode(tpl.content, true); root.appendChild(node); return root; }; util.getWatchId = function(url) { /\/?watch\/([a-z0-9]+)/.test(url || location.pathname); return RegExp.$1; }; util.isLogin = function() { return document.getElementsByClassName('siteHeaderLogin').length < 1; }; util.escapeHtml = function(text) { var map = { '&': '&', '\x27': ''', '"': '"', '<': '<', '>': '>' }; return text.replace(/[&"'<>]/g, char => { return map[char]; }); }; util.unescapeHtml = function(text) { var map = { '&' : '&' , ''' : '\x27', '"' : '"', '<' : '<', '>' : '>' }; return text.replace(/(&|'|"|<|>)/g, char => { return map[char]; }); }; util.hasLargeThumbnail = function(videoId) { // return true; // 大サムネが存在する最初の動画ID。 ソースはちゆ12歳 // ※この数字以降でもごく稀に例外はある。 var threthold = 16371888; var cid = videoId.substr(0, 2); if (cid !== 'sm') { return false; } var fid = videoId.substr(2) * 1; if (fid < threthold) { return false; } return true; }; util.httpLink = function(html) { const TMPLINK = ' '; let links = []; html = html.replace(/@([a-zA-Z0-9_]+)/g, (g, id) => { links.push( ` @${id} ` ); return TMPLINK; }); html = html.replace(/(im)(\d+)/g, ` $1$2 `); html = html.replace(/(co)(\d+)/g, ` $1$2 `); html = html.replace(/(watch|mylist|user)\/(\d+)/g, ` $1/$2 `); html = html.replace(/(sm|nm|so)(\d+)/g, ` $1$2 `); let linkmatch = //, n; html = html.split('
').join('
'); while ((n = linkmatch.exec(html)) !== null) { links.push(n); html = html.replace(n, TMPLINK); } html = html.replace(/\((https?:\/\/[\x21-\x3b\x3d-\x7e]+)\)/gi, '( $1 )'); html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)/gi, '$1'); for (var i = 0, len = links.length; i < len; i++) { html = html.replace(TMPLINK, links[i]); } html = html.split('
').join('
'); return html; }; util.getSleepPromise = function(sleepTime, label = 'sleep') { return function(result) { return new Promise(resolve => { console.time('sleep promise...' + label); window.setTimeout(() => { console.timeEnd('sleep promise...' + label); return resolve(result); }, sleepTime); }); }; }; util.getPageLanguage = function() { try { var h = document.getElementsByClassName('html')[0]; return h.lang || 'ja-JP'; } catch(e) { return 'ja-JP'; } }; const videoIdReg = /^[a-z]{2}\d+$/; util.getThumbnailUrlByVideoId = function(videoId) { if (!videoIdReg.test(videoId)) { return null; } const fileId = parseInt(videoId.substr(2), 10); const num = (fileId % 4) + 1; const large = util.hasLargeThumbnail(videoId) ? '.L' : ''; return '//tn-skr' + num + '.smilevideo.jp/smile?i=' + fileId + large; }; return util; })(); class Emitter { constructor() { } on(name, callback) { if (!this._events) { this._events = {}; } name = name.toLowerCase(); if (!this._events[name]) { this._events[name] = []; } this._events[name].push(callback); } clear(name) { if (!this._events) { this._events = {}; } if (name) { this._events[name] = []; } else { this._events = {}; } } emit(name) { if (!this._events) { this._events = {}; } name = name.toLowerCase(); if (!this._events.hasOwnProperty(name)) { return; } const e = this._events[name]; const arg = Array.prototype.slice.call(arguments, 1); for (let i =0, len = e.length; i < len; i++) { e[i].apply(null, arg); } } emitAsync(...args) { window.setTimeout(() => { this.emit(...args); }, 0); } } MylistPocket.emitter = util.emitter = new Emitter(); const ZenzaDetector = (function() { let isReady = false; let Zenza = null; const emitter = new Emitter(); const initialize = function() { const onZenzaReady = () => { isReady = true; Zenza = window.ZenzaWatch; Zenza.emitter.on('hideHover', () => { util.emitter.emit('hideHover'); }); Zenza.emitter.on('csrfToken', (token) => { util.emitter.emit('csrfToken', token); }); let popup = document.getElementById('mylistPocket-popup'); let defaultContainer = document.getElementById('mylistPocketDomContainer'); let zenzaContainer; Zenza.emitter.on('fullScreenStatusChange', isFull => { if (isFull) { if (!zenzaContainer) { zenzaContainer = document.querySelector('.zenzaPlayerContainer'); } zenzaContainer.appendChild(popup); } else { defaultContainer.appendChild(popup); } }); emitter.emit('ready', Zenza); }; if (window.ZenzaWatch && window.ZenzaWatch.ready) { window.console.log('ZenzaWatch is Ready'); onZenzaReady(); } else { window.jQuery('body').on('ZenzaWatchReady', function() { //document.body.addEventListener('ZenzaWatchReady', function() { window.console.log('onZenzaWatchReady'); onZenzaReady(); }); } }; const detect = function() { return new Promise(res => { if (isReady) { return res(Zenza); } emitter.on('ready', () => { res(Zenza); }); }); }; return { initialize: initialize, detect: detect }; })(); const StorageWriter = (function() { // マイページのJSON.stringifyがPrototype.jsのせいでぶっこわれているので // 汚染されていないWebWorkerを使って書き込む const func = function(self) { self.onmessage = function(e) { const key = e.data.key; const value = e.data.value; const storage = e.data.storage; self.postMessage({key, value: JSON.stringify(value), storage}); }; }; const worker = util.createWebWorker(func); worker.addEventListener('message', (e) => { const key = e.data.key; const value = e.data.value; const storage = e.data.storage === 'session' ? sessionStorage : localStorage; storage[key] = value; }); return { write: function({key, value, storage = 'local'}) { worker.postMessage({ key, value, storage }); } }; })(); MylistPocket.debug.writer = StorageWriter; const config = (function() { const prefix = PRODUCT + '_config_'; const emitter = new Emitter(); const defaultConfig = { debug: false }; const config = {}; let noEmit = false; Object.keys(defaultConfig).forEach(key => { var storageKey = prefix + key; if (localStorage.hasOwnProperty(storageKey)) { try { config[key] = JSON.parse(localStorage.getItem(storageKey)); } catch (e) { window.console.error('config parse error key:"%s" value:"%s" ', key, localStorage.getItem(storageKey), e); config[key] = defaultConfig[key]; } } else { config[key] = defaultConfig[key]; } }); emitter.getValue = function(key, refresh) { if (refresh) { emitter.refreshValue(key); } return config[key]; }; emitter.setValue = function(key, value) { if (config[key] !== value && arguments.length >= 2) { var storageKey = prefix + key; if (location.host === 'www.nicovideo.jp') { StorageWriter.write({key: storageKey, value}); //localStorage.setItem(storageKey, JSON.stringify(value)); } config[key] = value; console.log('%cconfig update "%s" = "%s"', 'background: cyan', key, value); } }; emitter.clearConfig = function() { noEmit = true; Object.keys(defaultConfig).forEach(key => { if (_.contains(['message', 'debug'], key)) { return; } var storageKey = prefix + key; try { if (localStorage.hasOwnProperty(storageKey)) { localStorage.removeItem(storageKey); } config[key] = defaultConfig[key]; } catch (e) {} }); noEmit = false; }; emitter.getKeys = function() { return Object.keys(defaultConfig); }; return emitter; })(); MylistPocket.config = config; const CacheStorage = (function() { var PREFIX = PRODUCT + '_cache_'; class CacheStorage { constructor(storage, gc = false) { this._storage = storage; this._memory = {}; if (gc) { this.gc(); } Object.keys(storage).forEach((key) => { if (key.indexOf(PREFIX) === 0) { this._memory[key] = storage[key]; } }); } gc() { const storage = this._storage; Object.keys(storage).forEach((key) => { if (key.indexOf(PREFIX) === 0) { let item; try { item = JSON.parse(this._storage[key]); } catch(e) { storage.removeItem(key); } //console.info( // `key: ${key}, expiredAt: ${item.expiredAt}, now: ${Date.now()}`); if (item.expiredAt === '' || item.expiredAt > Date.now()) { //console.info('not expired: ', key); return; } //console.info('cache expired: ', key, item.expiredAt); storage.removeItem(key); } }); } setItem(key, data, expireTime) { key = PREFIX + key; const expiredAt = typeof expireTime === 'number' ? (Date.now() + expireTime) : ''; const cacheData = { data: data, type: typeof data, expiredAt: expiredAt }; this._memory[key] = cacheData; StorageWriter.write({ key, value: cacheData, storage: this._storage === sessionStorage ? 'session' : 'local' }); //this._storage[key] = JSON.stringify(cacheData); } getItem(key) { key = PREFIX + key; if (!this._storage.hasOwnProperty(key)) { return null; } let item = null; try { item = JSON.parse(this._storage[key]); } catch(e) { delete this._memory[key]; this._storage.removeItem(key); return null; } if (item.expiredAt === '' || item.expiredAt > Date.now()) { return item.data; } return null; } removeItem(key) { if (this._memory.hasOwnProperty(key)) { delete this._memory[key]; } key = PREFIX + key; if (this._storage.hasOwnProperty(key)) { this._storage.removeItem(key); } } clear() { const storage = this._storage; this._memory = {}; Object.keys(storage).forEach((v) => { if (v.indexOf(PREFIX) === 0) { storage.removeItem(v); } }); } } return CacheStorage; })(); MylistPocket.debug.sessionCache = new CacheStorage(sessionStorage, true); MylistPocket.debug.localCache = new CacheStorage(localStorage, true); const WindowMessageEmitter = (function() { const emitter = new Emitter(); const knownSource = []; const onMessage = (event) => { if (_.indexOf(knownSource, event.source) < 0 //&& //event.origin !== location.protocol + '//ext.nicovideo.jp' ) { return; } try { var data = JSON.parse(event.data); if (data.id !== PRODUCT) { return; } emitter.emit('onMessage', data.body, data.type); } catch (e) { console.log( '%cMylistPocket.Error: window.onMessage - ', 'color: red; background: yellow', e, event ); console.log('%corigin: ', 'background: yellow;', event.origin); console.log('%cdata: ', 'background: yellow;', event.data); console.trace(); } }; emitter.addKnownSource = (win) => { knownSource.push(win); }; window.addEventListener('message', onMessage); return emitter; })(); const CrossDomainGate = (function() { class CrossDomainGate extends Emitter { constructor(params) { super(); this._baseUrl = params.baseUrl; this._origin = params.origin || location.href; this._type = params.type; this._messager = params.messager || WindowMessageEmitter; this._loaderFrame = null; this._sessions = {}; this._initializeStatus = ''; } _initializeFrame() { switch (this._initializeStatus) { case 'done': return new Promise((resolve) => { window.setTimeout(() => { resolve(); }, 0); }); case 'initializing': return new Promise((resolve, reject) => { this.on('initialize', (e) => { if (e.status === 'ok') { resolve(); } else { reject(e); } }); }); case '': this._initializeStatus = 'initializing'; var initialPromise = new Promise((resolve, reject) => { this._sessions.initial = { promise: initialPromise, resolve: resolve, reject: reject }; setTimeout(() => { if (this._initializeStatus !== 'done') { var rej = { status: 'fail', message: 'CrossDomainGate初期化タイムアウト (' + this._type + ')' }; reject(rej); this.emit('initialize', rej); } }, 60 * 1000); this._initializeCrossDomainGate(); }); return initialPromise; } } _initializeCrossDomainGate() { this._initializeCrossDomainGate = _.noop; this._messager.on('onMessage', this._onMessage.bind(this)); console.log('%c initialize ' + this._type, 'background: lightgreen;'); const loaderFrame = document.createElement('iframe'); loaderFrame.name = this._type + 'Loader'; loaderFrame.className = 'xDomainLoaderFrame ' + this._type; document.body.appendChild(loaderFrame); this._loaderFrame = loaderFrame; this._loaderWindow = loaderFrame.contentWindow; this._messager.addKnownSource(this._loaderWindow); this._loaderWindow.location.href = this._baseUrl + '#' + TOKEN; } _onMessage(data, type) { if (type !== this._type) { return; } const info = data.message; const token = info.token; const sessionId = info.sessionId; const status = info.status; const command = info.command || 'loadUrl'; let session = this._sessions[sessionId]; if (status === 'initialized') { this._initializeStatus = 'done'; this._sessions.initial.resolve(); this.emitAsync('initialize', {status: 'ok'}); return; } if (token !== TOKEN) { window.console.log('invalid token:', token, TOKEN); return; } switch (command) { case 'dumpConfig': this._onDumpConfig(info.body); break; default: if (!session) { return; } if (status === 'ok') { session.resolve(info.body); } else { session.reject({ message: status }); } session = null; delete this._sessions[sessionId]; break; } } load(url, options) { return this._postMessage({ command: 'loadUrl', url: url, options: options }, true); } _postMessage(message, needPromise) { return new Promise((resolve, reject) => { message.sessionId = this._type + '_' + Math.random(); message.token = TOKEN; if (needPromise) { this._sessions[message.sessionId] = { resolve: resolve, reject: reject }; } return this._initializeFrame().then(() => { try { this._loaderWindow.postMessage( JSON.stringify(message), this._origin ); } catch (e) { console.log('%cException!', 'background: red;', e); } }); }); } } return CrossDomainGate; })(); const CsrfTokenLoader = (() => { const cacheStorage = new CacheStorage( location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage); const TIMEOUT = 10 * 1000; const CACHE_EXPIRE_TIME = 60 * 30 * 1000; class CsrfTokenLoader { static load() { return new Promise((resolve, reject) => { const cache = cacheStorage.getItem('csrfToken'); if (cacheStorage.getItem('csrfToken')) { return resolve(cache); } let timeoutTimer = window.setTimeout(() => { reject('timeout'); }, TIMEOUT); return CsrfTokenLoader._getToken().then((token) => { window.clearTimeout(timeoutTimer); CsrfTokenLoader.saveToCache(token); resolve(token); }); }); } static saveToCache(token) { cacheStorage.setItem('csrfToken', token, CACHE_EXPIRE_TIME); } static _getToken() { const url = 'http://www.nicovideo.jp/mylist_add/video/sm9'; const tokenReg = /NicoAPI\.token *= *["']([a-z0-9\-]+)["'];/; return fetch(url, { credentials: 'include' }).then((res) => { return res.text(); }).then((result) => { if (tokenReg.test(result)) { let token = RegExp.$1; return Promise.resolve(token); } else { return Promise.reject('token parse error'); } }); } } util.emitter.on('csrfToken', (token) => { CsrfTokenLoader.saveToCache(token); }); return CsrfTokenLoader; })(); MylistPocket.debug.CsrfTokenLoader = CsrfTokenLoader; const ThumbInfoLoader = (() => { const BASE_URL = location.protocol + '//ext.nicovideo.jp/'; const MESSAGE_ORIGIN = location.protocol + '//ext.nicovideo.jp/'; let gate = null; let cacheStorage = new CacheStorage(localStorage); class ThumbInfoLoader { constructor() { this._emitter = new Emitter(); gate = new CrossDomainGate({ baseUrl: BASE_URL, origin: MESSAGE_ORIGIN, type: 'thumbInfo' + PRODUCT, messager: WindowMessageEmitter }); } _onMessage(data, type) { if (type !== 'videoInfoLoader') { return; } const info = data.message; this.emit('load', info, 'THUMB_WATCH'); } _parseXml(xmlText) { const parser = new DOMParser(); const xml = parser.parseFromString(xmlText, 'text/xml'); const val = (name) => { var elms = xml.getElementsByTagName(name); if (elms.length < 1) { return null; } return elms[0].innerHTML; }; const resp = xml.getElementsByTagName('nicovideo_thumb_response'); if (resp.length < 1 || resp[0].getAttribute('status') !== 'ok') { return { status: 'fail', code: val('code'), message: val('description') }; } const duration = (() => { const tmp = val('length').split(':'); return parseInt(tmp[0], 10) * 60 + parseInt(tmp[1], 10); })(); const watchId = val('watch_url').split('/').reverse()[0]; const postedAt = (new Date(val('first_retrieve'))).toLocaleString(); const tags = (() => { const result = [], t = xml.getElementsByTagName('tag'); _.each(t, (tag) => { result.push(tag.innerHTML); }); return result; })(); const result = { status: 'ok', _format: 'thumbInfo', v: watchId, id: val('video_id'), title: val('title'), description: val('description'), thumbnail: val('thumbnail_url'), movieType: val('movie_type'), lastResBody: val('last_res_body'), duration: duration, postedAt: postedAt, mylistCount: parseInt(val('mylist_counter'), 10), viewCount: parseInt(val('view_counter'), 10), commentCount: parseInt(val('comment_num'), 10), tagList: tags }; const userId = val('user_id'); if (userId !== null) { result.owner = { type: 'user', id: userId, name: val('user_nickname') || '(非公開ユーザー)', url: userId ? ('//www.nicovideo.jp/user/' + userId) : '#', icon: val('user_icon_url') || '//res.nimg.jp/img/user/thumb/blank.jpg' }; } const channelId = val('ch_id'); if (channelId !== null) { result.owner = { type: 'channel', id: channelId, name: val('ch_name') || '(非公開ユーザー)', url: '//ch.nicovideo.jp/ch' + channelId, icon: val('ch_icon_url') || '//res.nimg.jp/img/user/thumb/blank.jpg' }; } return result; } loadXml(watchId) { return this.load(watchId, 'xml'); } load(watchId, format) { return new Promise((resolve, reject) => { const cache = cacheStorage.getItem('thumbInfo_' + watchId); const onLoad = (xml) => { const result = this._parseXml(xml); if (result.status === 'ok') { if (!cache) { cacheStorage.setItem('thumbInfo_' + watchId, xml, 60 * 60 * 1000); } resolve({data: format === 'xml' ? xml : result, watchId}); } else { reject({data: format === 'xml' ? xml : result, watchId}); } }; if (cache) { //console.log('cache exist: ', watchId); window.setTimeout(() => { onLoad(cache); }, 0); return; } gate.load(BASE_URL + 'api/getthumbinfo/' + watchId).then(onLoad); }); } } const loader = new ThumbInfoLoader(); return { load: (watchId) => { return loader.load(watchId); }, loadXml: (watchId) => { return loader.loadXml(watchId); }, loadOwnerInfo: (watchId) => { return loader.load(watchId).then((info) => { const owner = info.data.owner; if (!owner) { return {}; } const lang = util.getPageLanguage(); const prefix = owner.type === 'user' ? '投稿者: ' : '提供: '; const suffix = (owner.type === 'user' && lang === 'ja-JP') ? ' さん' : ''; owner.localeName = `${prefix}${owner.name}${suffix}`; return owner; }); } }; })(); MylistPocket.debug.ThumbInfoLoader = ThumbInfoLoader; const DeflistApiLoader = ((CsrfTokenLoader) => { const cacheStorage = new CacheStorage( location.host === 'www.nicovideo.jp' ? localStorage : sessionStorage); const TIMEOUT = 30000; const CACHE_EXPIRE_TIME = 60 * 3 * 1000; let isZenzaReady = false; class DeflistApiLoader { static getItems() { const url = '//www.nicovideo.jp/api/deflist/list'; const cacheKey = 'deflistItems'; return new Promise(function(resolve, reject) { const cache = cacheStorage.getItem(cacheKey); if (cache) { window.setTimeout(() => { resolve({items: cache.mylistitem, status: cache.status, from: 'cache'}); }, 0); return; } let timeoutTimer = window.setTimeout(() => { timeoutTimer = null; reject({status: 'fail', description: 'timeout'}); }, TIMEOUT); fetch(url, { credentials: 'include' }).then((res) => { return res.json(); }).then((json) => { if (json.status !== 'ok') { return reject(json); } if (timeoutTimer) { window.clearTimeout(timeoutTimer); } else { return; } cacheStorage.setItem(cacheKey, json, CACHE_EXPIRE_TIME); resolve({items: json.mylistitem, status: json.status, from: 'fetch'}); }); }); } static findItemByWatchId(watchId) { return DeflistApiLoader.getItems().then(({items}) => { for (var i = 0, len = items.length; i < len; i++) { var item = items[i], wid = item.id || item.item_data.watch_id; if (wid === watchId) { return Promise.resolve(item); } } return Promise.reject(); }); } static _removeItem({watchId, token}) { const cacheKey = 'deflistItems'; DeflistApiLoader.findItemByWatchId(watchId).then((item) => { const url = '//www.nicovideo.jp/api/deflist/delete'; const body = 'id_list[0][]=' + item.item_id + '&token=' + token; const req = { credentials: 'include', method: 'post', body, headers: {'Content-Type': 'application/x-www-form-urlencoded'} }; return fetch(url, req) .then(res => { return res.json(); }) .then((result) => { if (result.status !== 'ok') { return Promise.reject({ status: 'fail', result: result, code: result.error.code, message: result.error.description }); } cacheStorage.removeItem(cacheKey); util.emitter.emitAsync('deflistRemove', watchId); return Promise.resolve({ status: 'ok', result: result, message: 'とりあえずマイリストから削除' }); }, (err) => { return Promise.reject({ result: err, message: 'とりあえずマイリストから削除失敗(2)' }); }); }, (err) => { return Promise.reject({ status: 'fail', result: err, message: '動画が見つかりません' }); }); } static removeItem(watchId) { return CsrfTokenLoader.load().then((token) => { return DeflistApiLoader._removeItem({watchId, token}); }); } static __addItem({watchId, description, token, isRetry = false}) { const cacheKey = 'deflistItems'; const url = '//www.nicovideo.jp/api/deflist/add'; let body = 'item_id=' + watchId + '&token=' + token; if (description) { body += '&description='+ encodeURIComponent(description); } const req = { method: 'post', credentials: 'include', body, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }; return new Promise((resolve, reject) => { fetch(url, req) .then((res) => { return res.json(); }) .then((result) => { if (result.status && result.status === 'ok') { cacheStorage.removeItem(cacheKey); //ZenzaWatch.emitter.emitAsync('deflistAdd', watchId, description); return resolve({ status: 'ok', result: result, message: 'とりあえずマイリスト登録' }); } if (!result.status || !result.error) { return reject({ status: 'fail', result: result, message: 'とりあえずマイリスト登録失敗(100)' }); } if (result.error.code !== 'EXIST' || isRetry) { return reject({ status: 'fail', result: result, code: result.error.code, message: result.error.description }); } /** * すでに登録されている場合は、いったん削除して再度追加(先頭に移動) */ return DeflistApiLoader.removeItem(watchId) .then(util.getSleepPromise(1500, 'deflist remove')) .then(() => { return DeflistApiLoader._addItem(watchId, description, true) .then((result) => { resolve({ status: 'ok', result: result, message: 'とりあえずマイリストの先頭に移動' }); }); }, (err) => { reject({ status: 'fail', result: err.result, code: err.code, message: 'とりあえずマイリスト登録失敗(101)' }); }); }, (err) => { reject({ status: 'fail', result: err, message: 'とりあえずマイリスト登録失敗(200)' }); }); }); } static _addItem(watchId, description, isRetry = false) { return CsrfTokenLoader.load().then((token) => { return DeflistApiLoader.__addItem({watchId, description, isRetry, token}); }); } static addItem(watchId, description) { return DeflistApiLoader._addItem(watchId, description, false); } static addItemWithOwnerName(watchId) { return ThumbInfoLoader.loadOwnerInfo(watchId).then((owner) => { if (!owner.id) { return DeflistApiLoader.addItem(watchId); } const description = owner.localeName; return DeflistApiLoader.addItem(watchId, description); }, () => { return DeflistApiLoader.addItem(watchId); }); // .then( // (result) => { console.log('ok', result); }, // (err) => { console.error('err', err); } //); } static clearCache() { cacheStorage.removeItem('deflistItems'); } } ZenzaDetector.detect().then((ZenzaWatch) => { isZenzaReady = true; ZenzaWatch.emitter.on('deflistRemove', () => { DeflistApiLoader.clearCache(); }); }); //DeflistApiLoader.clearCache(); return DeflistApiLoader; })(CsrfTokenLoader); MylistPocket.debug.DeflistApiLoader = DeflistApiLoader; class HoverMenu extends Emitter { constructor() { super(); this._init(); } _init() { this._view = document.querySelector('.mylistPocketHoverMenu'); this._view.addEventListener('click', this._onClick.bind(this)); $('body') .on('mouseover', 'a[href*="watch/"],a[href*="nico.ms/"]', this._onHover.bind(this)) .on('mouseover', 'a[href*="watch/"],a[href*="nico.ms/"]', _.debounce(this._onHoverEnd.bind(this), 500)) .on('mouseout', 'a[href*="watch/"],a[href*="nico.ms/"]', this._onMouseout.bind(this)) .on('click', () => { this.hide(); }); util.emitter.on('hideHover', () => { this.hide(); }); this._x = this._y = 0; ZenzaDetector.detect().then((ZenzaWatch) => { this._isZenzaReady = true; this.addClass('is-zenzaReady'); ZenzaWatch.emitter.on('DialogPlayerOpen', _.debounce(() => { this.hide(); }, 1000)); }); this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp'); this._deflistButton = this._view.querySelector('.mylistPocketButton.deflist-add'); MylistPocket.debug.hoverMenu = this._view; } toggleClass(className, v) { className.split(/ +/).forEach((c) => { this._view.classList.toggle(c, v); }); } addClass(className) { this.toggleClass(className, true); } removeClass(className) { this.toggleClass(className, false); } hide() { this.removeClass('is-show'); } show() { this.addClass('is-show'); } moveTo(x, y) { this._x = x; this._y = y; this._view.style.left = x + 'px'; this._view.style.top = y + 'px'; } _onClick(e) { const watchId = this._watchId; const target = e.target.classList.contains('command') ? e.target : e.target.closest('.command'); const command = target.getAttribute('data-command'); e.preventDefault(); e.stopPropagation(); if (command === 'info') { this._videoInfo(watchId); this.hide(); } else { this._deflist(watchId); } } _videoInfo(watchId) { this.emit('info', watchId || this._watchId, this); } _deflist(watchId) { this.emit('deflist-add', watchId || this._watchId, this); } _onHover(e) { this._hoverElement = e.target; } _onHoverEnd(e) { if (this._hoverElement !== e.target) { return; } const target = e.target.closest('a'); const $target = $(target); const href = target.getAttribute('data-href') || target.getAttribute('href'); const watchId = util.getWatchId(href); const offset = $target.offset(); const host = target.hostname; //console.info('onHoverEnd target=%s, href=%s, target=%s, href=%s, watchId=%s, host=%s', target, href, watchId, host, offset); if (host !== 'www.nicovideo.jp' && host !== 'nico.ms') { return; } //this._query = util.parseQuery(($target[0].search || '').substr(1)); if ($target.hasClass('noHoverMenu')) { return; } if (!watchId.match(/^[a-z0-9]+$/)) { return; } if (watchId.indexOf('lv') === 0) { return; } this._watchId = watchId; this.show(); this.moveTo( offset.left + target.offsetWidth - this._view.offsetWidth / 2, offset.top + target.offsetHeight / 2 - this._view.offsetHeight / 2 ); } _onMouseout(e) { if (this._hoverElement === e.target) { this._hoverElement = null; } } set isBusy(v) { this._isBusy = v; this.toggleClass('is-busy', v); } get isBusy() { return !!this._isBusy; } notifyBeginDeflistUpdate(/*watchId*/) { this.addClass('is-deflistUpdating'); } notifyEndDeflistUpdate(result) { this.addClass('is-deflistSuccess'); window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000); //window.console.info('ok result', result); this._deflistButton.setAttribute('data-result', result.message || '登録しました'); this.removeClass('is-deflistUpdating'); } notifyFailDeflistUpdate(result) { this.addClass('is-deflistFail'); window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000); //window.console.info('fail result', result); this._deflistButton.setAttribute('data-result', result.message || '登録失敗'); this.removeClass('is-deflistUpdating'); } } class VideoInfoView extends Emitter { constructor({host, tpl}) { super(); this._host = host; this._tpl = tpl; this._slot = {}; } _initialize() { if (this._isInitialized) { return; } const host = this._host; const tpl = this._tpl; this._shadowRoot = util.attachShadowDom({host, tpl}); this._host.querySelectorAll('*').forEach((elm) => { const slot = elm.getAttribute('slot'); if (!slot) { return; } //const type = elm.getAttribute('data-type') || 'string'; this._slot[slot] = elm; }); this._rootDom = this._shadowRoot.querySelector('.root'); this._hostDom = this._host; this._rootDom.addEventListener('mousedown', (e) => { e.stopPropagation(); }); //this._rootDom.querySelector('.description').addEventListener('mousewheel', (e) => { // e.preventDefault(); //}); this._rootDom.addEventListener('click', this._onClick.bind(this)); this._boundOnBodyMouseDown = this._onBodyMouseDown.bind(this); MylistPocket.debug.view = this; util.emitter.on('hideHover', () => { this.hide(); }); ZenzaDetector.detect().then(() => { this._isZenzaReady = true; this.addClass('is-zenzaReady'); window.ZenzaWatch.emitter.on('DialogPlayerOpen', _.debounce(() => { this.hide(); }, 1000)); }); this._videoInfoArea = this._rootDom.querySelector('.video-info'); this._deflistButton = this._rootDom.querySelector('.mylistPocketButton.deflist-add'); this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp'); this._isInitialized = true; } toggleClass(className, v) { className.split(/ +/).forEach((c) => { this._rootDom.classList.toggle(c, v); this._hostDom.classList.toggle(c, v); }); } addClass(className) { this.toggleClass(className, true); } removeClass(className) { this.toggleClass(className, false); } bind(videoInfo) { //console.info('status?', videoInfo.status, videoInfo.status === 'ok'); if (videoInfo.status === 'ok') { this._bindSuccess(videoInfo); } else { this._bindFail(videoInfo); } window.setTimeout(() => { this.removeClass('is-loading'); }, 0); } _onClick(e) { const t = e.target; const elm = t.classList.contains('command') ? t : e.target.closest('.command'); if (!elm) { return; } // 簡易 throttle if (elm.classList.contains('is-active')) { return; } elm.classList.add('is-active'); window.setTimeout(() => { elm.classList.remove('is-active'); }, 500); e.preventDefault(); e.stopPropagation(); const command = elm.getAttribute('data-command'); const param = elm.getAttribute('data-param'); this.emit('command', command, param, this); } _onBodyMouseDown() { document.body.removeEventListener('mousedown', this._boundOnBodyMouseDown); this.hide(); } reset() { this._initialize(); this._videoInfoArea.scrollTop = 0; this.removeClass('noclip'); this.addClass('is-loading'); } show() { this.addClass('show'); document.body.addEventListener('mousedown', this._boundOnBodyMouseDown); } hide() { this.removeClass('show is-ok is-fail noclip'); } _bindSuccess(videoInfo) { const toCamel = p => { return p.replace(/-./g, s => { return s.charAt(1).toUpperCase(); }); }; Object.keys(this._slot).forEach((key) => { const camelKey = toCamel(key); const data = videoInfo[camelKey]; //console.log('keys', typeof data, key, camelKey, data); if (typeof data !== 'string' && typeof data !== 'object') { return; } const elm = this._slot[key]; const type = elm.getAttribute('data-type') || 'string'; switch (type) { case 'html': this._createDescription(elm, data); break; case 'int': let i = parseInt(data, 10); i = i.toLocaleString ? i.toLocaleString() : i; elm.textContent = i; break; case 'link': elm.href = data; break; case 'image': elm.src = data; break; case 'date': elm.textContent = data.toLocaleString(); break; default: elm.textContent = data; } }); const df = document.createDocumentFragment(); this._host.querySelectorAll('.tag').forEach(t => { t.remove(); }); videoInfo.tags.forEach(tag => { df.appendChild((this._createTagSlot(tag))); }); this._host.appendChild(df); this._rootDom.querySelectorAll('.command-watch-id').forEach(elm => { elm.setAttribute('data-param', videoInfo.watchId); }); this._rootDom.querySelectorAll('.command-video-id').forEach(elm => { elm.setAttribute('data-param', videoInfo.videoId); }); this.toggleClass('is-channel', videoInfo.isChannel); this.addClass('is-ok'); this.removeClass('is-fail'); window.setTimeout(() => { this.addClass('noclip'); }, 1000); } _createDescription(elm, data) { elm.innerHTML = util.httpLink(data); const watchReg = /watch\/([a-z0-9]+)/; const isZenzaReady = this._isZenzaReady; elm.querySelectorAll('.videoLink[href*=\'watch/\']').forEach((link) => { const href = link.getAttribute('href'); if (!watchReg.test(href)) { return; } const watchId = RegExp.$1; if (isZenzaReady) { link.classList.add('noHoverMenu'); link.classList.add('command'); link.setAttribute('data-command', 'zenza-open'); link.setAttribute('data-param', watchId); } const btn = document.createElement('button'); btn.innerHTML = '?'; btn.className = 'command command-button noHoverMenu'; btn.setAttribute('slot', 'command-button'); btn.setAttribute('tooltip', '動画情報'); btn.setAttribute('data-command', 'info'); btn.setAttribute('data-param', watchId); link.appendChild(btn); const thumbnail = util.getThumbnailUrlByVideoId(watchId); if (thumbnail) { const img = document.createElement('img'); img.className = 'videoThumbnail'; img.src = thumbnail; link.classList.add('popupThumbnail'); link.appendChild(img); } link.classList.add('watch'); }); } _bindFail(videoInfo) { this._slot['error-description'].textContent = `動画情報の取得に失敗しました (${videoInfo.description})`; this.addClass('is-fail'); this.removeClass('is-ok'); } _createTagSlot(tag) { const text = util.escapeHtml(tag.text); const lock = tag.isLocked ? 'is-locked' : ''; const a = document.createElement('a'); a.textContent = tag.text; a.slot = 'tag'; a.className = `tag ${lock}`; a.href = `/tag/${encodeURIComponent(text)}`; return a; } notifyBeginDeflistUpdate(/*watchId*/) { this.addClass('is-deflistUpdating'); } notifyEndDeflistUpdate(result) { this.addClass('is-deflistSuccess'); window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000); //window.console.info('ok result', result); this._deflistButton.setAttribute('data-result', result.message || '登録しました'); this.removeClass('is-deflistUpdating'); } notifyFailDeflistUpdate(result) { this.addClass('is-deflistFail'); window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000); //window.console.info('fail result', result); this._deflistButton.setAttribute('data-result', result.message || '登録失敗'); this.removeClass('is-deflistUpdating'); } } class VideoInfo { static createByThumbInfo({xml, watchId}) { const dom = (new DOMParser()).parseFromString(xml, 'text/xml'); const status = dom.getElementsByTagName('nicovideo_thumb_response')[0].getAttribute('status'); //console.info('status', status); const t = function(name) { const tt = dom.getElementsByTagName(name); if (!tt || !tt[0]) { return ''; } return tt[0].textContent.trim(); }; const videoId = t('video_id'); let thumbnail = t('thumbnail_url'); if (util.hasLargeThumbnail(videoId)) { thumbnail += '.L'; } const isChannel = !!t('ch_id'); const tags = []; const rawData = { status, videoId: t('video_id'), watchId: watchId, videoTitle: t('title'), videoThumbnail: thumbnail, uploadDate: t('first_retrieve'), duration: t('length'), viewCounter: t('view_counter'), mylistCounter: t('mylist_counter'), commentCounter: t('comment_num'), description: t('description'), lastResBody: t('last_res_body'), isChannel, ownerId: isChannel ? t('ch_id') : t('user_id'), ownerName: isChannel ? t('ch_name') : t('user_nickname'), ownerIcon: isChannel ? t('ch_icon_url') : t('user_icon_url'), tags }; dom.querySelectorAll('tag').forEach(tag => { const isLocked = tag.getAttribute('lock'); const text = tag.textContent; tags.push({text, isLocked}); }); return new VideoInfo(rawData); } constructor(rawData) { this._rawData = rawData; } get status() { return this._rawData.status; } get videoId() { return this._rawData.videoId; } get watchId() { return this._rawData.watchId; } get videoTitle() { return this._rawData.videoTitle; } get videoThumbnail() { return this._rawData.videoThumbnail; } get description() { return this._rawData.description; } get duration() { return this._rawData.duration; } get ownerPageLink() { const ownerId = this.ownerId; if (this.isChannel) { return `//ch.nicovideo.jp/ch${ownerId}`; } else { return `//www.nicovideo.jp/user/${ownerId}`; } } get ownerIcon() { return this._rawData.ownerIcon; } get ownerName() { return this._rawData.ownerName; } get localeOwnerName() { if (this.isChannel) { return this.ownerName; } else { // TODO: 言語依存 return this.ownerName + ' さん'; } } get ownerId() { return this._rawData.ownerId; } get isChannel() { return this._rawData.isChannel; } get uploadDate() { return new Date(this._rawData.uploadDate); } get viewCounter() { return this._rawData.viewCounter; } get mylistCounter() { return this._rawData.mylistCounter; } get commentCounter() { return this._rawData.commentCounter; } get lastResBody() { return this._rawData.lastResBody; } get tags() { return this._rawData.tags; } } const deflistAdd = (watchId) => { if (location.host === 'www.nicovideo.jp') { return DeflistApiLoader.addItemWithOwnerName(watchId); } else { let zenza; let token; return ZenzaDetector.detect().then((z) => { zenza = z; }).then(() => { return CsrfTokenLoader.load().then((t) => { token = t; }, () => { return Promise.resolve(); }); }).then(() => { return ThumbInfoLoader.loadOwnerInfo(watchId); }).then((owner) => { //console.info(watchId, token, owner, zenza); if (!owner.id) { return zenza.external.deflistAdd(watchId); } const description = owner.localeName; return zenza.external.deflistAdd({watchId, description, token}); }); } }; const initDom = () => { util.addStyle(__css__); const f = document.createElement('div'); f.id = 'mylistPocketDomContainer'; f.innerHTML = __tpl__; document.body.appendChild(f); }; const initZenzaBridge = () => { ZenzaDetector.initialize(); }; const createVideoInfoView = () => { const host = document.getElementById('mylistPocket-popup'); const tpl = document.getElementById('mylistPocket-popup-template'); const vv = new VideoInfoView({host, tpl}); return vv; }; const createVideoInfoLoader = (vv) => { const onVideoInfoLoad = ({data, watchId}) => { const vi = VideoInfo.createByThumbInfo({xml: data, watchId}); vv.bind(vi); }; const onVideoInfoFail = () => { vv.bind({status: 'fail', description: '通信失敗'}); return Promise.resolve(); }; return function(watchId) { vv.reset(); vv.show(); return ThumbInfoLoader.loadXml(watchId).then( onVideoInfoLoad, onVideoInfoFail ); }; }; const createCommandDispatcher = ({infoView}) => { const load = createVideoInfoLoader(infoView); return (command, param, src) => { switch(command) { case 'info': return load(param); case 'mylist-window': window.open( '//www.nicovideo.jp/mylist_add/video/' + param, 'nicomylistadd', 'width=500, height=400, menubar=no, scrollbars=no'); break; case 'twitter-hash-open': window.open('https://twitter.com/hashtag/' + param + '?src=hash'); break; case 'zenza-open-now': window.ZenzaWatch.external.sendOrExecCommand('openNow', param); break; case 'zenza-open': window.ZenzaWatch.external.sendOrOpen(param); break; case 'playlist-inert': window.ZenzaWatch.external.playlist.insert(param); break; case 'playlist-queue': window.ZenzaWatch.external.playlist.add(param); break; case 'deflist-add': src.notifyBeginDeflistUpdate('is-deflistUpdating'); return deflistAdd(param) .then(util.getSleepPromise(1000, 'deflist-add')) .then((result) => { //console.info('deflist-add-result', result); //src.removeClass('is-deflistUpdating'); src.notifyEndDeflistUpdate(result); }, (err) => { console.error('deflist-add-result', err); src.notifyFailDeflistUpdate(err); }); } }; }; const initExternal = (dispatcher, hoverMenu, infoView) => { MylistPocket.external = { info: (watchId) => { dispatcher('info', watchId); }, hide: () => { hoverMenu.hide(); infoView.hide(); } }; MylistPocket.isReady = true; $('body').trigger('MylistPocketReady', MylistPocket); }; const init = () => { initDom(); initZenzaBridge(); const infoView = createVideoInfoView(); const dispatcher = createCommandDispatcher({infoView}); infoView.on('command', dispatcher); const hoverMenu = new HoverMenu(); hoverMenu.on('info', (watchId) => { hoverMenu.isBusy = true; dispatcher('info', watchId) .then(() => { hoverMenu.isBusy = false; }); }); hoverMenu.on('deflist-add', (watchId, src) => { dispatcher('deflist-add', watchId, src); }); MylistPocket.debug.hoverMenu = hoverMenu; initExternal(dispatcher, hoverMenu, infoView); }; init(); }; const postToParent = function(type, message, token) { const origin = document.referrer; //console.info('postToParent type=%s, message=%s, token=%s, origin=%s', // type, message, token, origin); try { parent.postMessage(JSON.stringify({ id: PRODUCT, type: type, body: { token: token, url: location.href, message: message } }), origin); } catch (e) { alert(e); console.log('err', e); } }; const thumbInfoApi = function() { if (window.name.indexOf('thumbInfo' + PRODUCT + 'Loader') < 0 ) { return; } window.console.log( '%cCrossDomainGate: %s %s', 'background: lightgreen;', PRODUCT, location.host); const parentHost = document.referrer.split('/')[2]; if (!parentHost.match(/^[a-z0-9]*.nicovideo.jp$/)) { window.console.log('disable bridge'); return; } const type = 'thumbInfo' + PRODUCT; const token = location.hash ? location.hash.substr(1) : null; location.hash = ''; window.addEventListener('message', (event) => { const data = JSON.parse(event.data); let timeoutTimer, isTimeout = false; if (data.token !== token) { return; } //window.console.log('child onMessage', data, event); if (!data.url) { return; } const sessionId = data.sessionId; fetch(data.url).then((resp) => { return resp.text(); }).then((text) => { if (isTimeout) { return; } else { window.clearTimeout(timeoutTimer); } try { postToParent(type, { sessionId: sessionId, status: 'ok', token: token, url: data.url, body: text }); } catch (e) { console.log( '%cError: parent.postMessage - ', 'color: red; background: yellow', e, event.origin, event.data); } }); timeoutTimer = window.setTimeout(() => { isTimeout = true; postToParent(type, { sessionId: sessionId, status: 'timeout', command: 'loadUrl', url: data.url }); }, 30000); }); try { postToParent(type, { status: 'initialized' }); } catch (e) { console.log('err', e); } }; const loadGm = function() { const script = document.createElement('script'); script.id = PRODUCT + 'Loader'; script.setAttribute('type', 'text/javascript'); script.setAttribute('charset', 'UTF-8'); script.appendChild(document.createTextNode( '(' + monkey + ')();' )); document.body.appendChild(script); }; var MIN_JQ = 10000600000; const getJQVer = function() { if (!window.jQuery) { return 0; } var ver = []; var t = window.jQuery.fn.jquery.split('.'); while(t.length < 3) { t.push(0); } _.each(t, (v) => { ver.push((v * 1 + 100000).toString().substr(1)); }); return ver.join('') * 1; }; const loadJq = function() { window.console.log('JQVer: ', getJQVer()); window.console.info('load jQuery from cdn...'); return new Promise((resolve, reject) => { var $j = window.jQuery || null; var $$ = window.$ || null; var script = document.createElement('script'); script.id = 'mp_jQueryLoader'; script.setAttribute('type', 'text/javascript'); script.setAttribute('charset', 'UTF-8'); script.src = 'https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js'; document.body.appendChild(script); var count = 0; var tm = window.setInterval(() => { count++; if (getJQVer() >= MIN_JQ) { window.clearInterval(tm); window.mpJQuery = window.jQuery; if ($j) { window.jQuery = $j; } if ($$) { window.$ = $$; } resolve(); } if (count >= 100) { window.clearInterval(tm); window.console.error('load jQuery timeout'); reject(); } }, 300); }); }; const host = window.location.host || ''; //const href = (location.href || '').replace(/#.*$/, ''); //const prot = location.protocol; if (host === 'ext.nicovideo.jp' && window.name.indexOf('thumbInfo' + PRODUCT + 'Loader') >= 0) { thumbInfoApi(); } else if (window === top) { if (getJQVer() >= MIN_JQ) { loadGm(); } else { loadJq().then(loadGm); } } })();