您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
動画をあとで見る + 簡易NG機能。 ZenzaWatchとの連携も可能。
当前为
- // ==UserScript==
- // @name MylistPocket
- // @namespace https://github.com/segabito/
- // @description 動画をあとで見る + 簡易NG機能。 ZenzaWatchとの連携も可能。
- // @match *://www.nicovideo.jp/*
- // @match *://ext.nicovideo.jp/
- // @match *://ext.nicovideo.jp/#*
- // @match *://ch.nicovideo.jp/*
- // @match *://com.nicovideo.jp/*
- // @match *://commons.nicovideo.jp/*
- // @match *://dic.nicovideo.jp/*
- // @match *://ex.nicovideo.jp/*
- // @match *://info.nicovideo.jp/*
- // @match *://search.nicovideo.jp/*
- // @match *://uad.nicovideo.jp/*
- // @match *://site.nicovideo.jp/*
- // @match *://anime.nicovideo.jp/*
- // @match https://www.google.com/search?*
- // @match https://www.google.co.jp/search?*
- // @match https://*.bing.com/*
- // @exclude *://ads*.nicovideo.jp/*
- // @exclude *://www.upload.nicovideo.jp/*
- // @exclude *://www.nicovideo.jp/watch/*?edit=*
- // @exclude *://ch.nicovideo.jp/tool/*
- // @exclude *://flapi.nicovideo.jp/*
- // @exclude *://dic.nicovideo.jp/p/*
- // @exclude *://ext.nicovideo.jp/thumb/*
- // @exclude *://ext.nicovideo.jp/thumb_channel/*
- // @version 0.5.3
- // @grant none
- // @author segabito macmoto
- // @license public domain
- // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js
- // ==/UserScript==
- /* eslint-disable */
- const AntiPrototypeJs = function() {
- if (this.promise || !window.Prototype || window.PureArray) {
- return this.promise || Promise.resolve(window.PureArray || window.Array);
- }
- const f = document.createElement('iframe');
- f.srcdoc = '<html><title>ここだけ時間が10年遅れてるスレ</title></html>';
- Object.assign(f.style, { position: 'absolute', left: '-100vw', top: '-100vh' });
- return this.promise = new Promise(res => {
- f.onload = res;
- document.documentElement.append(f);
- }).then(() => {
- window.PureArray = f.contentWindow.Array;
- delete window.Array.prototype.toJSON;
- delete window.Array.prototype.toJSON;
- delete window.String.prototype.toJSON;
- f.remove();
- return Promise.resolve(window.PureArray);
- });
- }.bind({promise: null});
- AntiPrototypeJs().then(() => {
- const PRODUCT = 'MylistPocket';
- const monkey = (PRODUCT) => {
- const console = window.console;
- const {workerUtil} = window.MylistPocketLib;
- //const $ = window.jQuery;
- console.log(`%c${PRODUCT}`,
- 'font-family: "Apple LiGothic"; padding: 4px; background: red; color: white; font-size: 150%;'
- );
- const TOKEN = 'r:' + (Math.random());
- const CONSTANT = {
- BASE_Z_INDEX: 100000
- };
- const MylistPocket = {debug: {}};
- window.MylistPocket = MylistPocket;
- const protocol = location.protocol;
- const __css__ = (`
- a[href*='watch/'] > g-img {
- position: inherit;
- }
- .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;
- }
- .mylistPocketHoverMenu.is-busy {
- opacity: 0 !important;
- pointer-events: none;
- }
- .mylistPocketHoverMenu.is-otherDomain .wwwOnly {
- display: none;
- }
- .mylistPocketHoverMenu.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly {
- display: none;
- }
- .mylistPocketHoverMenu .zenzaMenu {
- display: none;
- }
- .mylistPocketHoverMenu.is-zenzaReady .zenzaMenu {
- display: inline-block;
- }
- .mylistPocketButton {
- /*font-family: Menlo;*/
- display: block;
- font-weight: bolder;
- cursor: pointer;
- width: 32px;
- height: 26px;
- background: #ccc;
- color: black;
- 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 {
- display: none;
- perspective: 800px;
- }
- #mylistPocket-popup.is-firefox {
- /*perspective: none !important;*/
- position: fixed;
- z-index: 200000;
- transform: translate3d(-50%, -50%, 0);
- opacity: 0;
- transition: 0.3s opacity ease;
- top: -9999px; left: -9999px;
- }
- #mylistPocket-popup.show {
- display: block;
- }
- #mylistPocket-popup.is-firefox.show {
- top: 50%;
- left: 50%;
- opacity: 1;
- }
- #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 {
- position: relative;
- display: block;
- backface-visibility: hidden;
- }
- #mylistPocket-popup .description a[data-title]:hover::after {
- content: attr(data-title);
- position: absolute;
- top: -16px;
- left: 0;
- word-break: break-all;
- line-height: 12px;
- padding: 4px;
- font-size: 12px;
- color: #333;
- background: #ffc;
- opacity: 0.8;
- user-select: none;
- pointer-events: none;
- }
- #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
- ;
- }
- .zenzaPlayerContainer.is-error #mylistPocket-popup,
- .zenzaPlayerContainer.is-loading #mylistPocket-popup,
- .zenzaPlayerContainer.error #mylistPocket-popup,
- .zenzaPlayerContainer.loading #mylistPocket-popup {
- opacity: 0;
- pointer-events: none;
- }
- .mylistPocketHoverMenu.is-guest .is-need-login {
- display: none !important;
- }
- .xDomainLoaderFrame {
- position: fixed;
- left: -100%;
- top: -100%;
- width: 64px;
- height: 64px;
- opacity: 0;
- border: 0;
- }
- body.BaseLayout {
- margin-top: 0 !important;
- }
- #siteHeader {
- position: sticky;
- left: 0 !important;
- will-change: transform;
- }
- body.nofix #siteHeader {
- position: static;
- }
- .RankingMainContainer-header {
- position: sticky;
- top: 36px;
- z-index: 1000;
- background:
- linear-gradient(to bottom,
- rgba(255, 255, 255, 0),
- rgba(255, 255, 255, 0.7),
- rgba(255, 255, 255, 1.0),
- rgba(255, 255, 255, 0.8),
- rgba(232, 232, 255, 0)
- );
- }
- .nofix .RankingMainContainer-header {
- top: 0;
- }
- .RankingBaseItem {
- border-radius: 0 !important;
- box-shadow: none !important;
- border: 1px solid silver;
- pointer-events: none;
- user-select: none;
- display: grid;
- }
- .RankingBaseItem .Card-link {
- display: grid;
- grid-template-rows: 108px auto;
- }
- .RankingBaseItem .Card-media {
- position: static;
- pointer-events: auto;
- }
- .VideoThumbnail {
- border-radius: 0 !important;
- }
- .RankingBaseItem .Card-title {
- pointer-events: auto;
- user-select: auto;
- height: auto;
- max-height: 49px;
- -webkit-line-clamp: unset;
- }
- .RankingBaseItem .Card-secondary {
- width: 100%;
- user-select: none;
- pointer-events: none;
- align-self: end;
- overflow: hidden;
- }
- [data-nicoad-grade=gold] .Thumbnail.VideoThumbnail {
- background: #f7e01c;
- }
- [data-nicoad-grade=silver] .Thumbnail.VideoThumbnail {
- background: #dfeaec;
- }
- .MatrixRanking-body.GlobalHeader#siteHeader #siteHeaderInner {
- width: 1232px;
- }
- .MatrixRanking-body .RankingRowRank {
- line-height: 48px;
- height: 48px;
- pointer-events: none;
- user-select: none;
- }
- .MatrixRanking-body .RankingMatrixVideosRow {
- width: ${1232 + 64}px;
- margin-left: ${-64}px;
- }
- .MatrixRanking-body .RankingRowRank {
- position: sticky;
- left: -8px;
- z-index: 100;
- transform: none;
- padding-right: 16px;
- width: 64px;
- overflow: visible;
- text-align: right;
- mix-blend-mode: difference;
- text-shadow:
- 1px 1px 0 #fff,
- 1px -1px 0 #fff,
- -1px 1px 0 #fff,
- -1px -1px 0 #fff;
- }
- `).trim();
- const nicoadHideCss = `
- .nicoadVideoItem {
- display: none;
- }
- .MatrixRankingBannerAd,
- .RankingMatrixNicoadsRow, .RankingMainNicoad {
- display: none;
- }
- `.trim();
- const responsiveCss = `
- @media screen and (max-width: 1350px) {
- .RankingGenreListContainer-categoryHelp {
- position: static;
- }
- .GlobalHeader#siteHeader #siteHeaderInner {
- width: 1024px;
- }
- .MatrixRanking-body .BaseLayout-block {
- width: ${1024 + 64 * 2}px;
- }
- .RankingMainContainer-decorateChunk+.RankingMainContainer-decorateChunk,
- .RankingMainContainer-decorateChunk>*+* {
- margin-top: 0;
- }
- .RankingMainContainer {
- width: ${1024}px;
- }
- .MatrixRanking-body .RankingMatrixVideosRow {
- width: ${1024 + 64}px;
- margin-left: ${-64}px;
- }
- .RankingMatrixNicoadsRow>*+*,
- .RankingMatrixVideosRow>:nth-child(n+3) {
- margin-left: 13px;
- }
- .RankingBaseItem {
- width: 160px;
- height: 196px;
- }
- .RankingBaseItem .Card-link {
- grid-template-rows: 90px auto;
- }
- .VideoItem.RankingBaseItem .VideoThumbnail {
- border-radius: 3px 3px 0 0;
- }
- [data-nicoad-grade] .Thumbnail.VideoThumbnail .Thumbnail-image {
- margin: 3px;
- background-size: calc(100% + 6px);
- }
- [data-nicoad-grade] .Thumbnail.VideoThumbnail:after {
- width: 40px;
- height: 40px;
- background-size: 80px 80px;
- }
- .Thumbnail.VideoThumbnail .VideoLength {
- bottom: 3px;
- right: 3px;
- }
- .VideoThumbnailComment {
- transform: scale(0.8333);
- }
- .RankingBaseItem-meta {
- position: static;
- padding: 0 4px 8px;
- }
- .VideoItem.RankingBaseItem .VideoItem-metaCount>.VideoMetaCount {
- white-space: nowrap;
- }
- .RankingMainContainer .ToTopButton {
- transform: translateX(calc(100vw / 2 - 100% - 36px));
- user-select: none;
- }
- }
- `;
- const __tpl__ = (`
- <div class="mylistPocketHoverMenu scalingUI zen-family">
- <button class="mylistPocketButton command deflist-add wwwZenzaOnly is-need-login" data-command="deflist"
- tooltip="とりあえずマイリスト">✚</button>
- <button class="mylistPocketButton command info" data-command="info"
- tooltip="動画情報を表示">?</button>
- <button class="mylistPocketButton command playlist-queue zenzaMenu" data-command="playlist-queue"
- tooltip="ZenzaWatchのプレイリストに追加">▶</button>
- </div>
- </div>
- <div id="mylistPocket-popup" class="zen-family">
- <span slot="video-title">【実況】どんぐりころころの大冒険 Part1(最終回)</span>
- <a href="/watch/sm9" slot="watch-link"></a>
- <img slot="video-thumbnail" data-type="image">
- <a slot="owner-page-link" href="https://www.nicovideo.jp/user/1234" class="owner-page-link target-change" data-type="link" rel="noopener"><img slot="owner-icon" class="owner-icon" src="https://nicovideo.cdn.nimg.jp/web/img/user/thumb/blank_s.jpg" data-type="image"></img></a>
- <span slot="upload-date" data-type="date">1970/01/01 00:00</span>
- <span slot="view-counter" data-type="int">12,345</span>
- <span slot="mylist-counter" data-type="int">6,789</span>
- <span slot="comment-counter" data-type="int">2,525</span>
- <span slot="duration" class="duration">1:23</span>
- <span slot="owner-id">1234</span>
- <span slot="locale-owner-name">ほげほげ</span>
- <div slot="error-description"></div>
- <div class="description" slot="description" data-type="html"></div>
- <span slot="last-res-body"></span>
- </div>
- <template id="mylistPocket-popup-template">
- <style>
- :host(#mylistPocket-popup) {
- position: fixed;
- z-index: 200000;
- transform: translate3d(-50%, -50%, 0);
- opacity: 0;
- transition: 0.3s opacity ease;
- top: -9999px; left: -9999px;
- }
- :host(#mylistPocket-popup.show) {
- top: 50%;
- left: 50%;
- opacity: 1;
- pointer-events: auto;
- }
- .root.is-otherDomain .wwwOnly {
- display: none;
- }
- .root.is-otherDomain:not(.is-zenzaReady) .wwwZenzaOnly {
- display: none;
- }
- * {
- box-sizing: border-box;
- font-kerning: none;
- }
- a {
- color: #ffff00;
- font-weight: bold;
- display: inline-block;
- }
- a:visited {
- color: #ffff99;
- }
- button {
- font-size: 14px;
- padding: 8px 8px;
- cursor: pointer;
- border-radius: 0;
- margin: 0;
- background: #333;
- color: #ccc;
- border: solid 2px #ccc;
- outline: none;
- line-height: 20px;
- user-select: none;
- -webkit-user-select: none;
- -moz-user-select: none;
- }
- button:hover {
- transform: translate(-4px,-4px);
- box-shadow: 4px 4px 4px #000;
- background: #666;
- transition:
- 0.2s transform ease,
- 0.2s box-shadow ease
- ;
- }
- button.is-updating {
- cursor: wait;
- }
- button.is-active,
- button:active {
- transform: none;
- box-shadow: none;
- transition: none;
- }
- button.is-active::after,
- button:active::after {
- opacity: 0;
- }
- [tooltip] {
- position: relative;
- }
- .is-deflistUpdating .deflist-add::after,
- .is-deflistSuccess .deflist-add::after,
- .is-deflistFail .deflist-add::after,
- [tooltip]:hover::after {
- content: attr(tooltip);
- position: absolute;
- top: 0px;
- left: 50%;
- padding: 2px 4px;
- white-space: nowrap;
- font-size: 14px;
- color: #fff;
- background: #333;
- transform: translate3d(-50%, -120%, 0);
- pointer-events: none;
- }
- .root {
- text-align: left;
- outline-offset: 8px;
- border: 12px solid rgba(32, 32, 32, 0);
- border-radius: 20px;
- padding: 8px 0;
- background: rgba(0, 0, 0, 0.7);
- color: #ccc;
- box-shadow: 0 0 16px #000;
- transition:
- 0.6s -webkit-clip-path ease,
- 0.6s clip-path ease,
- 0.5s transform ease;
- /*0.4s border-radius ease-out 0.4s,
- 0.4s height ease-out 0.4s*/
- ;
- }
- .root * {
- }
- .root.show {
- opacity: 1;
- pointer-events: auto !important;
- }
- .root.is-loading,
- .root.is-loading.is-ok,
- .root.is-loading.is-fail {
- text-align: center;
- position: relative;
- width: 190px;
- height: 190px;
- padding: 32px;
- opacity: 0.8;
- cursor: wait;
- border-radius: 100%;
- clip-path: circle(100px at center) !important;
- transition: none;
- outline: none;
- transform: none !important;
- }
- .root.is-firefox {
- }
- .root.is-loading > * {
- pointer-events: none;
- }
- .root.is-setting {
- transform: rotateX(180deg);
- }
- .root.is-setting > *:not(.setting-panel) {
- pointer-events: none;
- z-index: 1;
- }
- .root:not(.is-setting) > .setting-panel {
- pointer-events: none;
- }
- .root.is-setting > .setting-panel {
- display: block;
- opacity: 1;
- pointer-events: auto;
- }
- .root.is-loading .loading-inner,
- .root.is-loading.is-ok .loading-inner,
- .root.is-loading.is-fail .loading-inner {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate3d(-50%, -50%, 0);
- }
- .loading-inner .spinner {
- font-size: 64px;
- display: inline-block;
- animation-name: spin;
- animation-iteration-count: infinite;
- animation-duration: 3s;
- animation-timing-function: linear;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(1800deg); }
- }
- .root.is-ok {
- width: 800px;
- /*clip-path: circle(800px at center);*/
- }
- .root.is-ok.noclip {
- clip-path: none;
- }
- .root.is-fail {
- font-size: 120%;
- white-space: nowrap;
- text-align: center;
- padding: 16px;
- }
- .root.is-loading>*:not(.loading-now),
- .root.is-loading.is-ok>*:not(.loading-now),
- .root.is-loading.is-fail>*:not(.loading-now),
- .root.is-fail:not(.is-loading)>*:not(.error-info),
- .root.is-ok:not(.is-loading)>*:not(.video-detail):not(.setting-panel) {
- display: none !important;
- }
- .root.is-loading>.loading-now,
- .root.is-fail>.error-info,
- .root.is-ok>.video-detail {
- display: block;
- }
- .header {
- padding: 8px 8px 8px;
- font-size: 12px;
- }
- .upload-date {
- margin-right: 8px;
- }
- .counter span + span {
- margin-left: 8px;
- }
- .video-title {
- font-weight: bolder;
- font-size: 22px;
- margin-bottom: 4px;
- }
- .close-button {
- position: absolute;
- right: 0;
- top: 0;
- transition: 0.2s background ease, 0.2s border-color ease;
- cursor: pointer;
- width: 48px;
- height: 48px;
- font-size: 28px;
- line-height: 36px;
- text-align: center;
- user-select: none;
- border: 6px solid rgba(80, 80, 80, 0.5);
- border-color: transparent;
- border-radius: 0 16px 0 0;
- }
- .close-button:hover {
- background: #333;
- /*border-color: rgba(0, 0, 0, 0.9);*/
- /*transform: translate(-50%, -50%) scale(2.5);*/
- }
- .close-button:active {
- /*transform: translate(-50%, -50%) scale(2) rotate(360deg);*/
- box-shadow: none;
- transition: none;
- }
- .is-setting .close-button {
- display: none;
- }
- .main {
- display: flex;
- background: rgba(0, 0, 0, 0.2);
- box-shadow: 0 0 4px rgba(0, 0, 0, 0.5) inset;
- }
- .main-left {
- width: 360px;
- padding: 8px;
- z-index: 100;
- }
- .video-thumbnail-container {
- position: relative;
- width: 360px;
- height: 270px;
- background: #000;
- /*box-shadow: 2px 2px 4px #000;*/
- }
- .video-thumbnail-container ::slotted(img) {
- width: 360px !important;
- height: 270px !important;
- object-fit: contain;
- }
- .video-thumbnail-container .duration {
- position: absolute;
- display: inline-block;
- right: 0;
- bottom: 0;
- font-size: 14px;
- background: #000;
- color: #fff;
- padding: 2px 4px;
- }
- .video-thumbnail-container:hover .duration {
- display: none;
- }
- .main-right {
- position: relative;
- padding: 0;
- flex-grow: 1;
- font-size: 14px;
- }
- ::slotted(.owner-page-link) {
- display: inline-block;
- vertical-align: middle;
- }
- .owner-page-link img {
- border: 1px solid #333;
- border-radius: 3px;
- }
- .video-info {
- /*background: rgba(0, 0, 0, 0.2);*/
- max-height: 282px;
- overflow-x: hidden;
- overflow-y: scroll;
- overscroll-behavior: contain;
- }
- *::-webkit-scrollbar,
- .video-info::-webkit-scrollbar {
- background: rgba(34, 34, 34, 0.5);
- }
- *::-webkit-scrollbar-thumb,
- .video-info::-webkit-scrollbar-thumb {
- border-radius: 0;
- background: #666;
- }
- *::-webkit-scrollbar-button,
- .video-info::-webkit-scrollbar-button {
- background: #666;
- display: none;
- }
- *::scrollbar,
- .video-info::scrollbar {
- background: #222;
- }
- *::scrollbar-thumb,
- .video-info::scrollbar-thumb {
- border-radius: 0;
- background: #666;
- }
- *::scrollbar-button,
- .video-info::scrollbar-button {
- background: #666;
- display: none;
- }
- .scrollable {
- overscroll-behavior: contain;
- }
- .owner-info {
- margin: 16px;
- display: table;
- }
- .owner-info * {
- vertical-align: middle;
- word-break: break-all;
- }
- .owner-info>* {
- display: table-cell !important;
- }
- .owner-name {
- display: inline-block;
- padding: 8px;
- font-size: 18px;
- }
- .owner-info.is-favorited {
- font-weight: bolder;
- color: orange;
- }
- .owner-info.is-ng {
- color: #888;
- text-decoration: line-through;
- }
- .is-channel .owner-name::before {
- content: 'CH';
- margin: 0 4px;
- background: #999;
- color: #333;
- padding: 2px 4px;
- border: 1px solid;
- }
- .locale-owner-name::after {
- content: ' さん';
- }
- .owner-info .add-ng-button,
- .owner-info .add-fav-button {
- visibility: hidden;
- pointer-events: none;
- }
- .is-ng-enable .owner-info:hover .add-ng-button,
- .is-ng-enable .owner-info:hover .add-fav-button {
- visibility: visible;
- pointer-events: auto;
- }
- .description {
- word-break: break-all;
- line-height: 1.5;
- padding: 0 16px 8px;
- }
- .description:first-letter {
- font-size: 24px;
- }
- .last-res-body {
- margin: 16px 16px 0;
- border: 1px solid #ccc;
- padding: 4px;
- border-radius: 4px;
- word-break: break-all;
- font-size: 12px;
- min-height: 24px;
- }
- .footer {
- padding: 8px;
- backface-visibility: hidden;
- }
- .pocket-button {
- cusror: pointer;
- }
- .pocket-button:active {
- }
- .video-tags {
- display: block;
- }
- .tag-container {
- display: inline-block;
- position: relative;
- padding: 4px 8px;
- border: 1px solid #888;
- border-radius: 4px;
- margin: 0 20px 4px 0;
- }
- .tag-container .tag {
- display: inline-block;
- font-size: 14px;
- color: #ccc;
- text-decoration: none;
- cursor: pointer;
- }
- .tag-container .tag.channel-search {
- margin-left: 8px;
- color: #ccc !important;
- padding: 0 8px;
- }
- .tag-container:hover .tag {
- color: #fff !important;
- }
- .tag-container.is-favorited .tag {
- font-weight: bolder;
- color: orange !important;
- }
- .tag-container.is-ng .tag {
- text-decoration: line-through;
- color: #888 !important;
- }
- .zenzaPlayerContainer .tagItemMenu {
- margin: 0 8px;
- }
- .tag-container .add-ng-button,
- .tag-container .add-fav-button {
- position: absolute !important;
- visibility: hidden;
- pointer-events: none;
- }
- .is-ng-enable .tag-container:hover .add-ng-button,
- .is-ng-enable .tag-container:hover .add-fav-button {
- visibility: visible;
- pointer-events: auto;
- width: 24px;
- height: 24px;
- line-height: 24px;
- font-size: 24px;
- vertical-align: bottom;
- display: inline-block;
- }
- .is-ng-enable .tag-container:hover .add-ng-button {
- right: -16px;
- }
- .is-ng-enable .tag-container:hover .add-fav-button {
- left: -16px;
- }
- .footer-menu {
- position: absolute;
- right: 0px;
- bottom: 0px;
- transform: translate3d(0, 120%, 0);
- opacity: 1;
- transition:
- 0.4s opacity ease 0.4s,
- 0.4s transform ease 0.4s;
- }
- .is-setting .video-detail .footer-menu {
- transform: translate3d(0, 0, 0);
- opacity: 0;
- }
- .footer-menu button {
- min-width: 70px;
- }
- .regular-menu {
- display: inline-block;
- background: rgba(0, 0, 0, 0.7);
- position: relative;
- border-radius: 8px;
- padding: 12px 16px;
- box-shadow: 0 0 16px #000;
- }
- .is-deflistUpdating .deflist-add {
- cursor: wait;
- opacity: 0.9;
- transform: scale(1.0);
- box-shadow: none;
- transition: none;
- }
- .is-deflistSuccess .deflist-add,
- .is-deflistFail .deflist-add {
- transform: scale(1.0);
- box-shadow: none;
- transition: none;
- }
- .is-deflistSuccess .deflist-add::after {
- content: attr(data-result);
- background: #393;
- }
- .is-deflistFail .deflist-add::after {
- content: attr(data-result);
- background: #933;
- }
- .is-deflistUpdating .deflist-add::after {
- content: '更新中';
- background: #333;
- }
- .zenza-menu {
- display: none;
- }
- .is-zenzaReady .zenza-menu {
- display: inline-block;
- background: rgba(0, 0, 0, 0.7);
- margin-left: 32px;
- position: relative;
- border-radius: 8px;
- padding: 12px 16px;
- box-shadow: 0 0 16px #000;
- }
- .is-zenzaReady .zenza-menu::after {
- content: 'ZenzaWatch';
- position: absolute;
- left: 50%;
- bottom: 10px;
- padding: 2px 8px;
- transform: translate(-50%, 100%);
- pointer-events: none;
- font-weith: bolder;
- background: rgba(0, 0, 0, 0.7);
- pointer-events: none;
- border-radius: 4px;
- white-space: nowrap;
- }
- .setting-menu {
- display: inline-block;
- background: rgba(0, 0, 0, 0.7);
- margin-left: 32px;
- position: relative;
- border-radius: 8px;
- padding: 12px 16px;
- box-shadow: 0 0 16px #000;
- }
- .toggle-setting-button {
- font-size: 32px;
- border-radius: 100%;
- border: 12px solid #333;
- cursor: pointer;
- background: rgba(32, 32, 32, 1);
- transition:
- 0.2s transform ease
- ;
- }
- .toggle-setting-button:hover {
- transform: scale(1.2);
- box-shadow: none;
- background: rgba(32, 32, 32, 1);
- background: transparent;
- }
- .toggle-setting-button:active {
- transform: scale(1.0);
- }
- .mylist-comment-link {
- cursor: pointer;
- }
- .setting-panel {
- opacity: 0;
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- padding: 8px 12px;
- z-index: 10000;
- background: rgba(50, 50, 64, 0.9);
- border-radius: 16px;
- color: #ccc;
- /*-webkit-user-select: none;
- user-select: none;*/
- transform: rotateX(180deg);
- transition: 0.25s opacity ease 0.25s;
- }
- .is-setting .setting-panel {
- transition: 0.25s opacity ease;
- }
- .setting-panel-main {
- width: 100%;
- height: 100%;
- overflow-y: scroll;
- overflow-x: hidden;
- }
- .root:not(.is-setting) .setting-panel .footer-menu {
- transform: translate3d(0, 0, 0);
- opacity: 0;
- }
- .root.is-setting .setting-panel .footer-menu {
- right: -12px;
- bottom: -12px;
- transform: translate3d(0, 120%, 0);
- opacity: 1;
- transition:
- opacity 0.4s ease 0.4s,
- transform 0.4s ease 0.4s;
- }
- .close-setting-menu {
- display: inline-block;
- background: rgba(0, 0, 0, 0.7);
- margin-left: 32px;
- position: relative;
- border-radius: 8px;
- padding: 12px 16px;
- box-shadow: 0 0 16px #000;
- }
- .setting-label {
- display: inline-block;
- line-height: 24px;
- padding: 8px;
- }
- .setting-label:hover {
- text-shadow: 0 0 4px #996;
- }
- .setting-label * {
- cursor: pointer;
- }
- .setting-label input[type=checkbox] {
- transform: scale(2);
- margin: 8px;
- vertical-align: middle;
- }
- .setting-label input + span {
- font-size: 16px;
- }
- .setting-label input:checked + span {
- }
- .setting-fav,
- .setting-ng-textarea,
- .setting-fav-textarea {
- display: none;
- }
- .is-ng-enable .setting-fav {
- display: block;
- }
- .is-ng-enable .setting-ng-textarea,
- .is-ng-enable .setting-fav-textarea {
- display: flex;
- }
- .setting-ng-text-column,
- .setting-fav-text-column {
- flex: 1;
- position: relative;
- padding: 8px;
- }
- .setting-ng-text-column textarea,
- .setting-fav-text-column textarea {
- width: 100%;
- height: 150px;
- background: transparent;
- color: #ccc;
- }
- .setting-ng-label {
- display: none;
- }
- .is-ng-enable .setting-ng-label {
- display: inline-block;
- }
- .add-ng-button,
- .add-fav-button {
- display: none;
- }
- .is-ng-enable .add-ng-button,
- .is-ng-enable .add-fav-button {
- display: inline-block;
- position: relative;
- width: 32px;
- height: 32px;
- line-height: 32px;
- font-size: 28px;
- padding: 0;
- margin: 0;
- /*border-radius: 100%;*/
- border: none;
- text-align: center;
- color: red;
- font-weight: bolder;
- cursor: pointer;
- background: transparent;
- box-shadow: none;
- transition:
- 0.2s transform ease,
- 0.2s text-shadow ease;
- }
- .is-ng-enable .add-fav-button {
- color: orange;
- }
- .is-ng-enable .add-ng-button:hover,
- .is-ng-enable .add-fav-button:hover {
- transform: scale(1.2);
- text-shadow: 2px 2px 4px black;
- }
- .is-ng-enable .add-ng-button:active,
- .is-ng-enable .add-fav-button:active {
- transform: scale(1.0);
- text-shadow: 0 0 2px black;
- }
- .is-ng-enable .add-ng-button:hover::after,
- .is-ng-enable .add-fav-button:hover::after {
- content: 'NG登録';
- position: absolute;
- top: 0;
- left: 50%;
- transform: translate(-50%, -80%);
- font-size: 12px;
- line-height: 12px;
- white-space: nowrap;
- background: rgba(192, 192, 192, 0.8);
- color: #000;
- opacity: 0.9;
- padding: 2px 4px;
- text-shadow: none;
- font-weight: normal;
- pointer-evnets: none !important;
- }
- .is-ng-enable .is-ng .add-ng-button:hover::after,
- .is-ng-enable .is-ng .add-fav-button:hover::after {
- content: 'NG解除';
- }
- .is-ng-enable .add-fav-button:hover::after {
- content: '強調登録';
- }
- .is-ng-enable .is-favorited .add-fav-button:hover::after {
- content: '強調解除';
- }
- .is-ng-enable .add-ng-button:active:hover::after,
- .is-ng-enable .add-fav-button:active:hover::after {
- display: none;
- }
- </style>
- <div class="popup root">
- <div class="loading-now">
- <div class="loading-inner">
- <span class="spinner">⌛</span>
- </div>
- </div>
- <div class="error-info">
- <slot name="error-description"></slot>
- </div>
- <div class="video-detail">
- <div class="header">
- <div class="video-title"><slot name="video-title"></slot></div>
- <span class="upload-date">投稿: <slot name="upload-date"/></span>
- <span class="counter">
- <span class="view-counter">再生: <slot name="view-counter"/></span>
- <span class="comment-counter">コメント: <slot name="comment-counter"/></span>
- <span class="mylist-counter command2" data-command="mylist-comment-open">マイリスト:
- <span class="mylist-comment-link command" data-command="mylist-comment-open">❏</span>
- <slot name="mylist-counter"/>
- </span>
- </span>
- <div class="close-button command" data-command="close" tooltip="閉じる">
- ✖
- </div>
- </div>
- <div class="main">
- <div class=" main-left">
- <div class="video-thumbnail-container">
- <slot name="video-thumbnail"></slot>
- <span class="duration"><slot name="duration"></slot></slot>
- </div>
- </div>
- <div class="video-info main-right scrollable">
- <div class="owner-info">
- <slot name="owner-page-link"></slot>
- <span class="owner-name"><slot name="locale-owner-name"></slot>
- <button class="add-fav-button command" data-command="toggle-fav-owner">★</button>
- <button class="add-ng-button command" data-command="toggle-ng-owner">✖</button>
- </span>
- </div>
- <div class="description">
- <slot name="description"></slot>
- </div>
- <div class="last-res-body">
- <slot name="last-res-body"></slot>
- </div>
- </div>
- </div>
- <div class="footer">
- <div class="video-tags">
- <slot name="tag"></slot>
- </div>
- </div>
- <div class="footer-menu scalingUI">
- <div class="regular-menu">
- <button
- class="mylistPocketButton deflist-add pocket-button command command-watch-id wwwZenzaOnly"
- data-command="deflist-add"
- tooltip="とりあえずマイリスト"
- >とり</button>
- <button
- class="pocket-button command command-watch-id"
- data-command="mylist-window"
- tooltip="マイリスト"
- >マイ</button>
- <button
- class="pocket-button command command-watch-id"
- data-command="open-mylist-open"
- tooltip="公開マイリスト"
- >公開</button>
- <button
- class="pocket-button command command-video-id"
- data-command="twitter-hash-open"
- tooltip="Twitterの反応"
- >#Twitter</button>
- </div>
- <div class="zenza-menu">
- <button
- class="pocket-button command command-watch-id"
- data-command="zenza-open-now"
- tooltip="ZenzaWatchで開く"
- >Zen</button>
- <button
- class="pocket-button command command-watch-id"
- data-command="playlist-inert"
- tooltip="プレイリスト(次に再生)"
- >playlist</button>
- <button
- class="pocket-button command command-watch-id"
- data-command="playlist-queue"
- tooltip="プレイリスト(末尾に追加)"
- >▶</button>
- </div>
- <div class="setting-menu">
- <button
- class="pocket-button command"
- data-command="toggle-setting"
- >設 定</button>
- </div>
- </div>
- </div>
- <div class="setting-panel">
- <div class="setting-panel-main scrollable">
- <h2>MylistPocket 設定</h2>
- <label class="setting-label">
- <input
- type="checkbox"
- class="setting-form"
- data-config-name="openNewWindow"
- >
- <span>タグやリンクを新しいタブで開く (次回から反映)</span>
- </label>
- <label class="setting-label">
- <input
- type="checkbox"
- class="setting-form"
- data-config-name="enableAutoComment"
- data-config-namespace="mylist"
- >
- <span>マイリストコメントに投稿者名を入れる</span>
- </label>
- <label class="setting-label">
- <input
- type="checkbox"
- class="setting-form"
- data-config-name="responsive.matrix"
- data-config-namespace=""
- >
- <span>ランキングTOPのサムネイルを画面幅に合わせて小さくする</span>
- </label>
- <h2>NG設定(リロード後に反映)</h2>
- <label class="setting-label">
- <input
- type="checkbox"
- class="setting-form"
- data-config-name="enable"
- data-config-namespace="ng"
- >
- <span>簡易NG&強調機能を使う</span>
- </label>
- <label class="setting-label">
- <input
- type="checkbox"
- class="setting-form"
- data-config-name="hide"
- data-config-namespace="nicoad"
- >
- <span>検索結果やランキングのニコニ広告を消す</span>
- </label>
- <label class="setting-label wwwOnly wwwZenzaOnly setting-ng-label">
- <input
- type="checkbox"
- class="setting-form"
- data-config-name="syncZenza"
- data-config-namespace="ng"
- >
- <span>NGタグ・投稿者をZenzaWatchにも反映する</span>
- </label>
- <div class="setting-ng-textarea setting-ng">
- <div class="setting-ng-text-column">
- 投稿者ID
- <textarea
- class="setting-form"
- data-config-name="owner"
- data-config-namespace="ng"
- ></textarea>
- </div>
- <div class="setting-ng-text-column">
- タグ
- <textarea
- class="setting-form"
- data-config-name="tag"
- data-config-namespace="ng"
- ></textarea>
- </div>
- <div class="setting-ng-text-column">
- タイトル・説明文
- <textarea
- class="setting-form"
- data-config-name="word"
- data-config-namespace="ng"
- ></textarea>
- </div>
- </div>
- <h2 class="setting-fav">強調表示設定</h2>
- <div class="setting-fav-textarea setting-fav">
- <div class="setting-fav-text-column">
- 投稿者ID
- <textarea
- class="setting-form"
- data-config-name="owner"
- data-config-namespace="fav"
- ></textarea>
- </div>
- <div class="setting-fav-text-column">
- タグ
- <textarea
- class="setting-form"
- data-config-name="tag"
- data-config-namespace="fav"
- ></textarea>
- </div>
- <div class="setting-fav-text-column">
- タイトル・説明文
- <textarea
- class="setting-form"
- data-config-name="word"
- data-config-namespace="fav"
- ></textarea>
- </div>
- </div>
- </div>
- <div class="footer-menu">
- <div class="close-setting-menu">
- <button
- class="pocket-button command"
- data-command="toggle-setting"
- >戻 る</button>
- </div>
- </div>
- </div>
- </div>
- </template>
- `).trim();
- const __ng_css__ = `
- /* .item_cell 将棋盤ランキング .item 従来のランキングと検索 */
- .RankingMainVideo.is-ng-wait,
- .RankingBaseItem.is-ng-wait,
- .item_cell.is-ng-wait .item,
- .item.is-ng-wait {
- outline: 1px dotted rgba(192, 192, 192, 0.8);
- }
- .RankingMainVideo.is-ng-queue,
- .RankingBaseItem.is-ng-queue,
- .item_cell.is-ng-queue .item,
- .item.is-ng-queue {
- outline: 2px dotted rgba(192, 192, 192, 0.8);
- }
- .RankingMainVideo.is-ng-current,
- .RankingBaseItem.is-ng-current,
- .item_cell.is-ng-current .item,
- .item.is-ng-current {
- outline: 3px dotted rgba(128, 225, 128, 0.8);
- }
- .RankingMainVideo.is-ng-resolved,
- .RankingBaseItem.is-ng-resolved,
- .item_cell.is-ng-resolved .item,
- .item.is-ng-resolved {
- outline: 0px solid green;
- }
- .RankingMainVideo.is-ng-favorited,
- .RankingBaseItem.is-ng-favorited,
- .item_cell.is-fav-favorited .item,
- .item.is-fav-favorited {
- outline: 3px dotted orange;
- outline-offset: 3px;
- }
- .item.videoRanking.is-fav-favorited {
- outline-offset: -3px;
- }
- .RankingBaseItem.is-ng-rejected,
- .item_cell.is-ng-rejected {
- opacity: 0;
- pointer-events: none;
- visibility: hidden;
- }
- .VideoItem .VideoItem-postDate {
- line-height: 16px;
- vertical-align: top;
- font-size: 12px;
- color: #666;
- }
- .RankingMainVideo.is-ng-rejected,
- .item.is-ng-rejected {
- display: none;
- opacity: 0;
- pointer-events: none;
- }
- .NicorepoTimelineItem.is-ng-rejected {
- display: none;
- opacity: 0;
- pointer-events: none;
- }
- body.is-ng-disable .is-ng-rejected {
- outline: none;
- display: block !important;
- pointer-events: auto;
- opacity: 0.5;
- visibility: visible;
- }
- /* チャンネル検索 */
- #search .item.is-ng-rejected {
- display: none;
- }
- `;
- // TODO: ライブラリ化
- const util = MylistPocket.util = (() => {
- const util = {};
- util.mixin = function(self, o) {
- Object.keys(o).forEach(f => {
- if (!_.isFunction(o[f])) { return; }
- if (_.isFunction(self[f])) { return; }
- self[f] = o[f].bind(o);
- });
- };
- util.attachShadowDom = function({host, tpl, mode = 'open'}) {
- const root = host.attachShadow ?
- host.attachShadow({mode}) : host.createShadowRoot();
- const node = document.importNode(tpl.content, true);
- root.appendChild(node);
- return root;
- };
- util.httpLink = function(html) {
- let links = {}, keyCount = 0;
- const getTmpKey = function() { return ` <!--${keyCount++}--> `; };
- html = html.replace(/@([a-zA-Z0-9_]+)/g,
- (g, id) => {
- const tmpKey = getTmpKey();
- links[tmpKey] =
- ` <a href="https://twitter.com/${id}" class="twitterLink" rel="noopener" target="_blank">@${id}</a> `;
- return tmpKey;
- });
- html = html.replace(/(im)(\d+)/g,
- ' <a href="//seiga.nicovideo.jp/seiga/$1$2" class="seigaLink" rel="noopener" target="_blank">$1$2</a> ');
- html = html.replace(/(co)(\d+)/g,
- ' <a href="//com.nicovideo.jp/community/$1$2" class="communityLink" rel="noopener" target="_blank">$1$2</a> ');
- html = html.replace(/(watch|mylist|user)\/(\d+)/g, ' <a href="https://www.nicovideo.jp/$1/$2" rel="noopener" class="videoLink target-change">$1/$2</a> ');
- html = html.replace(/(sm|nm|so)(\d+)/g, ' <a href="https://www.nicovideo.jp/watch/$1$2" rel="noopener" class="videoLink target-change">$1$2</a> ');
- let linkmatch = /<a.*?<\/a>/, n;
- html = html.split('<br />').join(' <br /> ');
- while ((n = linkmatch.exec(html)) !== null) {
- let tmpKey = getTmpKey();
- links[tmpKey] = n;
- html = html.replace(n, tmpKey);
- }
- html = html.replace(/\((https?:\/\/[\x21-\x3b\x3d-\x7e]+)\)/gi, '( $1 )');
- html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)http/gi, '$1 http');
- html = html.replace(/(https?:\/\/[\x21-\x3b\x3d-\x7e]+)/gi, '<a href="$1" rel="noopener" target="_blank" class="otherSite">$1</a>');
- Object.keys(links).forEach(tmpKey => {
- html = html.replace(tmpKey, links[tmpKey]);
- });
- html = html.split(' <br /> ').join('<br />');
- return html;
- };
- util.getSleepPromise = function(sleepTime, label = 'sleep') {
- return function(result) {
- return new Promise(resolve => {
- window.setTimeout(() => {
- return resolve(result);
- }, sleepTime);
- });
- };
- };
- util.isFirefox = () =>
- navigator.userAgent.toLowerCase().indexOf('firefox') >= 0;
- return util;
- })();
- const css = {
- addStyle: (styles, option, document = window.document) => {
- const elm = document.createElement('style');
- elm.type = 'text/css';
- if (typeof option === 'string') {
- elm.id = option;
- } else if (option) {
- Object.assign(elm, option);
- }
- elm.classList.add(PRODUCT);
- const text = document.createTextNode(styles.toString());
- elm.appendChild(text);
- (document.head || document.body || document.documentElement).append(elm);
- elm.disabled = option && option.disabled;
- elm.dataset.switch = elm.disabled ? 'off' : 'on';
- return elm;
- }
- };
- Object.assign(util, css);
- Object.assign(util, workerUtil);
- const nicoUtil = {
- parseWatchQuery: query => {
- try {
- const result = textUtil.parseQuery(query);
- const playlist = JSON.parse(textUtil.decodeBase64(result.playlist));
- if (playlist.searchQuery) {
- const sq = playlist.searchQuery;
- if (sq.type === 'tag') {
- result.playlist_type = 'tag';
- result.tag = sq.query;
- } else {
- result.playlist_type = 'search';
- result.keyword = sq.query;
- }
- let [order, sort] = (sq.sort || '+f').split('');
- result.order = order === '-' ? 'a' : 'd';
- result.sort = sort;
- if (sq.fRange) { result.f_range = sq.fRange; }
- if (sq.lRange) { result.l_range = sq.lRange; }
- } else if (playlist.mylistId) {
- result.playlist_type = 'mylist';
- result.group_id = playlist.mylistId;
- result.order =
- document.querySelector('select[name="sort"]') ?
- document.querySelector('select[name="sort"]').value : '1';
- } else if (playlist.id && playlist.id.includes('temporary_mylist')) {
- result.playlist_type = 'deflist';
- result.group_id = 'deflist';
- result.order =
- document.querySelector('select[name="sort"]') ?
- document.querySelector('select[name="sort"]').value : '1';
- }
- return result;
- } catch(e) {
- return {};
- }
- },
- hasLargeThumbnail: videoId => {
- const threthold = 16371888;
- const cid = videoId.substr(0, 2);
- const fid = videoId.substr(2) * 1;
- if (cid === 'nm') { return false; }
- if (cid !== 'sm' && fid < 35000000) { return false; }
- if (fid < threthold) {
- return false;
- }
- return true;
- },
- getThumbnailUrlByVideoId: videoId => {
- const videoIdReg = /^[a-z]{2}\d+$/;
- if (!videoIdReg.test(videoId)) {
- return null;
- }
- const fileId = parseInt(videoId.substr(2), 10);
- const large = nicoUtil.hasLargeThumbnail(videoId) ? '.L' : '';
- return fileId >= 35374758 ? // このIDから先は新サーバー(おそらく)
- `https://nicovideo.cdn.nimg.jp/thumbnails/${fileId}/${fileId}.L` :
- `https://tn.smilevideo.jp/smile?i=${fileId}.${large}`;
- },
- getWatchId: url => {
- let m;
- if (url && url.indexOf('nico.ms') >= 0) {
- m = /\/\/nico\.ms\/([a-z0-9]+)/.exec(url);
- } else {
- m = /\/?watch\/([a-z0-9]+)/.exec(url || location.pathname);
- }
- return m ? m[1] : null;
- },
- isPremium: () => {
- const h = document.getElementById('siteHeaderNotification');
- return h && h.classList.contains('siteHeaderPremium');
- },
- isLogin: () => document.getElementsByClassName('siteHeaderLogin').length < 1,
- getPageLanguage: () => {
- try {
- let h = document.getElementsByClassName('html')[0];
- return h.lang || 'ja-JP';
- } catch (e) {
- return 'ja-JP';
- }
- },
- openMylistWindow: watchId => {
- window.open(
- `//www.nicovideo.jp/mylist_add/video/${watchId}`,
- 'nicomylistadd',
- 'width=500, height=400, menubar=no, scrollbars=no');
- },
- openTweetWindow: ({watchId, duration, isChannel, title, videoId}) => {
- const nicomsUrl = `https://nico.ms/${watchId}`;
- const watchUrl = `https://www.nicovideo.jp/watch/${watchId}`;
- title = `${title}(${textUtil.secToTime(duration)})`.replace(/@/g, '@ ');
- const nicoch = isChannel ? ',+nicoch' : '';
- const url =
- 'https://twitter.com/intent/tweet?' +
- 'url=' + encodeURIComponent(nicomsUrl) +
- '&text=' + encodeURIComponent(title) +
- '&hashtags=' + encodeURIComponent(videoId + nicoch) +
- '&original_referer=' + encodeURIComponent(watchUrl) +
- '';
- window.open(url, '_blank', 'width=550, height=480, left=100, top50, personalbar=0, toolbar=0, scrollbars=1, sizable=1', 0);
- },
- isGinzaWatchUrl: url => /^https?:\/\/www\.nicovideo\.jp\/watch\//.test(url || location.href),
- getPlayerVer: () => {
- if (!document.getElementById('js-initial-watch-data')) {
- return 'html5';
- }
- if (document.getElementById('watchAPIDataContainer')) {
- return 'flash';
- }
- return 'unknown';
- },
- isZenzaPlayableVideo: () => {
- try {
- if (nicoUtil.getPlayerVer() === 'html5') {
- return true;
- }
- const watchApiData = JSON.parse(document.querySelector('#watchAPIDataContainer').textContent);
- const flvInfo = textUtil.parseQuery(
- decodeURIComponent(watchApiData.flashvars.flvInfo)
- );
- const dmcInfo = JSON.parse(
- decodeURIComponent(watchApiData.flashvars.dmcInfo || '{}')
- );
- const videoUrl = flvInfo.url ? flvInfo.url : '';
- const isDmc = dmcInfo && dmcInfo.time;
- if (isDmc) {
- return true;
- }
- const isSwf = /\/smile\?s=/.test(videoUrl);
- const isRtmp = (videoUrl.indexOf('rtmp') === 0);
- return (isSwf || isRtmp) ? false : true;
- } catch (e) {
- return false;
- }
- },
- getNicoHistory: window.decodeURIComponent(document.cookie.replace(/^.*(nicohistory[^;+]).*?/, ''))
- };
- Object.assign(util, nicoUtil);
- const textUtil = {
- secToTime: sec => {
- return [
- Math.floor(sec / 60).toString().padStart(2, '0'),
- (Math.floor(sec) % 60).toString().padStart(2, '0')
- ].join(':');
- },
- parseQuery: (query = '') => {
- query = query.startsWith('?') ? query.substr(1) : query;
- const result = {};
- query.split('&').forEach(item => {
- const sp = item.split('=');
- const key = decodeURIComponent(sp[0]);
- const val = decodeURIComponent(sp.slice(1).join('='));
- result[key] = val;
- });
- return result;
- },
- parseUrl: url => {
- url = url || 'https://unknown.example.com/';
- const a = document.createElement('a');
- a.href = url;
- return a;
- },
- decodeBase64: str => {
- try {
- return decodeURIComponent(
- escape(atob(
- str.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(str.length / 4) * 4, '=')
- )));
- } catch(e) {
- return '';
- }
- },
- encodeBase64: str => {
- try {
- return btoa(unescape(encodeURIComponent(str)));
- } catch(e) {
- return '';
- }
- },
- escapeHtml: text => {
- const map = {
- '&': '&',
- '\x27': ''',
- '"': '"',
- '<': '<',
- '>': '>'
- };
- return text.replace(/[&"'<>]/g, char => map[char]);
- },
- unescapeHtml: text => {
- const map = {
- '&': '&',
- ''': '\x27',
- '"': '"',
- '<': '<',
- '>': '>'
- };
- return text.replace(/(&|'|"|<|>)/g, char => map[char]);
- },
- escapeToZenkaku: text => {
- const map = {
- '&': '&',
- '\'': '’',
- '"': '”',
- '<': '<',
- '>': '>'
- };
- return text.replace(/["'<>]/g, char => map[char]);
- },
- escapeRegs: text => {
- const match = /[\\^$.*+?()[\]{}|]/g;
- return text.replace(match, '\\$&');
- },
- convertKansuEi: text => {
- let match = /[〇一二三四五六七八九零壱弐惨伍]/g;
- let map = {
- '〇': '0', '零': '0',
- '一': '1', '壱': '1',
- '二': '2', '弐': '2',
- '三': '3', '惨': '3',
- '四': '4',
- '五': '5', '伍': '5',
- '六': '6',
- '七': '7',
- '八': '8',
- '九': '9',
- };
- text = text.replace(match, char => map[char]);
- text = text.replace(/([1-9]?)[十拾]([0-9]?)/g, (n, a, b) => (a && b) ? `${a}${b}` : (a ? a * 10 : 10 + b * 1));
- return text;
- },
- dateToString: date => {
- if (typeof date === 'string') {
- const origDate = date;
- date = date.replace(/\//g, '-');
- const m = /^(\d+-\d+-\d+) (\d+):(\d+):(\d+)/.exec(date);
- if (m) {
- date = new Date(m[1]);
- date.setHours(m[2]);
- date.setMinutes(m[3]);
- date.setSeconds(m[4]);
- } else {
- const t = Date.parse(date);
- if (isNaN(t)) {
- return origDate;
- }
- date = new Date(t);
- }
- } else if (typeof date === 'number') {
- date = new Date(date);
- }
- if (!date || isNaN(date.getTime())) {
- return '1970/01/01 00:00:00';
- }
- const [yy, mm, dd, h, m, s] = [
- date.getFullYear(),
- date.getMonth() + 1,
- date.getDate(),
- date.getHours(),
- date.getMinutes(),
- date.getSeconds()
- ].map(n => n.toString().padStart(2, '0'));
- return `${yy}/${mm}/${dd} ${h}:${m}:${s}`;
- },
- isValidJson: data => {
- try {
- JSON.parse(data);
- return true;
- } catch (e) {
- return false;
- }
- },
- toRgba: (c, alpha = 1) =>
- `rgba(${parseInt(c.substr(1, 2), 16)}, ${parseInt(c.substr(3, 2), 16)}, ${parseInt(c.substr(5, 2), 16)}, ${alpha})`,
- snakeToCamel: snake => snake.replace(/-./g, s => s.charAt(1).toUpperCase()),
- camelToSnake: (camel, separator = '_') => camel.replace(/([A-Z])/g, s => separator + s.toLowerCase())
- };
- Object.assign(util, textUtil);
- const reg = (() => {
- const $ = Symbol('$');
- const undef = Symbol.for('undefined');
- const MAX_RESULT = 30;
- const smap = new WeakMap();
- const self = {};
- const reg = function(regex = undef, str = undef) {
- const {results, last} = smap.has(this) ?
- smap.get(this) : {results: [], last: {result: null}};
- smap.set(this, {results, last});
- if (regex === undef) {
- return last ? last.result : null;
- }
- const regstr = regex.toString();
- if (str !== undef) {
- const found = results.find(r => regstr === r.regstr && str === r.str);
- return found ? found.result : reg(regex).exec(str);
- }
- return {
- exec(str) {
- const result = regex.exec(str);
- Array.isArray(result) && result.forEach((r, i) => result['$' + i] = r);
- Object.assign(last, {str, regstr, result});
- results.push(last);
- results.length > MAX_RESULT && results.shift();
- this[$] = str[$] = regex[$] = result;
- return result;
- },
- test(str) { return !!this.exec(str); }
- };
- };
- const scope = (scopeObj = {}) => reg.bind(scopeObj);
- return Object.assign(reg.bind(self), {$, scope});
- })();
- 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');
- defaultContainer.classList.add('zen-family');
- 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 {
- document.body.addEventListener('ZenzaWatchInitialize', function() {
- window.console.log('ZenzaWatchInitialize MylistPocket');
- onZenzaReady();
- });
- }
- };
- const detect = function() {
- return new Promise(res => {
- if (isReady) {
- return res(Zenza);
- }
- emitter.on('ready', () => {
- res(Zenza);
- });
- });
- };
- return {
- initialize: initialize,
- detect: detect
- };
- })();
- const objUtil = (() => {
- const isObject = e => e !== null && e instanceof Object;
- return {
- bridge: (self, target, keys = null) => {
- (keys || Object.getOwnPropertyNames(target.constructor.prototype))
- .filter(key => typeof target[key] === 'function')
- .forEach(key => self[key] = target[key].bind(target));
- },
- isObject,
- toMap: (obj, mapper = Map) => {
- if (obj instanceof mapper) {
- return obj;
- }
- const map = new mapper();
- for(const key of Object.keys(map)) {
- map.set(key, obj[key]);
- }
- return map;
- },
- mapToObj: map => {
- if (!(obj instanceof Map)) {
- return map;
- }
- const obj = {};
- for (const [key, val] of map) {
- obj[key] = val;
- }
- return obj;
- }
- };
- })();
- const StorageWriter = (() => {
- const func = function(self) {
- self.onmessage = ({command, params}) => {
- const {obj, replacer, space} = params;
- return JSON.stringify(obj, replacer || null, space || 0);
- };
- };
- let worker;
- const prototypePollution = window.Prototype && Array.prototype.hasOwnProperty('toJSON');
- const toJson = async (obj, replacer = null, space = 0) => {
- if (!prototypePollution || obj === null || ['string', 'number', 'boolean'].includes(typeof obj)) {
- return JSON.stringify(obj, replacer, space);
- }
- worker = worker || workerUtil.createCrossMessageWorker(func, {name: 'ToJsonWorker'});
- return worker.post({command: 'toJson', params: {obj, replacer, space}});
- };
- const writer = Symbol('StorageWriter');
- const setItem = (storage, key, value) => {
- if (!prototypePollution || value === null || ['string', 'number', 'boolean'].includes(typeof value)) {
- storage.setItem(key, JSON.stringify(value));
- } else {
- toJson(value).then(json => storage.setItem(key, json));
- }
- };
- localStorage[writer] = (key, value) => setItem(localStorage, key, value);
- sessionStorage[writer] = (key, value) => setItem(sessionStorage, key, value);
- return { writer, toJson };
- })();
- const Observable = (() => {
- const observableSymbol = Symbol.observable || Symbol('observable');
- const nop = Handler.nop;
- class Subscription {
- constructor({observable, subscriber, unsubscribe, closed}) {
- this.callbacks = {unsubscribe, closed};
- this.observable = observable;
- const next = subscriber.next.bind(subscriber);
- subscriber.next = args => {
- if (this.closed || (this._filterFunc && !this._filterFunc(args))) {
- return;
- }
- return this._mapFunc ? next(this._mapFunc(args)) : next(args);
- };
- this._closed = false;
- }
- subscribe(subscriber, onError, onCompleted) {
- return this.observable.subscribe(subscriber, onError, onCompleted)
- .filter(this._filterFunc)
- .map(this._mapFunc);
- }
- unsubscribe() {
- this._closed = true;
- if (this.callbacks.unsubscribe) {
- this.callbacks.unsubscribe();
- }
- return this;
- }
- dispose() {
- return this.unsubscribe();
- }
- filter(func) {
- const _func = this._filterFunc;
- this._filterFunc = _func ? func : arg => func(_func(arg));
- return this;
- }
- map(func) {
- const _func = this._mapFunc;
- this._mapFunc = _func ? func : arg => func(_func(arg));
- return this;
- }
- get closed() {
- if (this.callbacks.closed) {
- return this._closed || this.callbacks.closed();
- } else {
- return this._closed;
- }
- }
- }
- class Subscriber {
- static create(onNext = null, onError = null, onCompleted = null) {
- if (typeof onNext === 'function') {
- return new this({
- next: onNext,
- error: onError,
- complete: onCompleted
- });
- }
- return new this(onNext || {});
- }
- constructor({start, next, error, complete} = {start:nop, next:nop, error:nop, complete:nop}) {
- this.callbacks = {start, next, error, complete};
- }
- start(arg) {this.callbacks.start(arg);}
- next(arg) {this.callbacks.next(arg);}
- error(arg) {this.callbacks.error(arg);}
- complete(arg) {this.callbacks.complete(arg);}
- get closed() {
- return this._callbacks.closed ? this._callbacks.closed() : false;
- }
- }
- Subscriber.nop = {start: nop, next: nop, error: nop, complete: nop, closed: nop};
- const eleMap = new WeakMap();
- class Observable {
- static of(...args) {
- return new this(o => {
- for (const arg of args) {
- o.next(arg);
- }
- o.complete();
- return () => {};
- });
- }
- static from(arg) {
- if (arg[Symbol.iterator]) {
- return this.of(...arg);
- } else if (arg[Observable.observavle]) {
- return arg[Observable.observavle]();
- }
- }
- static fromEvent(element, eventName) {
- const em = eleMap.get(element) || {};
- if (em && em[eventName]) {
- return em[eventName];
- }
- eleMap.set(element, em);
- return em[eventName] = new this(o => {
- const onUpdate = e => o.next(e);
- element.addEventListener(eventName, onUpdate, {passive: true});
- return () => element.removeEventListener(eventName, onUpdate);
- });
- }
- static interval(ms) {
- return new this(function(o) {
- const timer = setInterval(() => o.next(this.i++), ms);
- return () => clearInterval(timer);
- }.bind({i: 0}));
- }
- constructor(subscriberFunction) {
- this._subscriberFunction = subscriberFunction;
- this._completed = false;
- this._cancelled = false;
- this._handlers = new Handler();
- }
- _initSubscriber() {
- if (this._subscriber) {
- return;
- }
- const handlers = this._handlers;
- this._completed = this._cancelled = false;
- return this._subscriber = new Subscriber({
- start: arg => handlers.execMethod('start', arg),
- next: arg => handlers.execMethod('next', arg),
- error: arg => handlers.execMethod('error', arg),
- complete: arg => {
- if (this._nextObservable) {
- this._nextObservable.subscribe(this._subscriber);
- this._nextObservable = this._nextObservable._nextObservable;
- } else {
- this._completed = true;
- handlers.execMethod('complete', arg);
- }
- },
- closed: () => this.closed
- });
- }
- get closed() {
- return this._completed || this._cancelled;
- }
- filter(func) {
- return this.subscribe().filter(func);
- }
- map(func) {
- return this.subscribe().map(func);
- }
- concat(arg) {
- const observable = Observable.from(arg);
- if (this._nextObservable) {
- this._nextObservable.concat(observable);
- } else {
- this._nextObservable = observable;
- }
- return this;
- }
- forEach(callback) {
- let p = new PromiseHandler();
- callback(p);
- return this.subscribe({
- next: arg => {
- p.resolve(arg);
- p = new PromiseHandler();
- callback(p);
- },
- error: arg => {
- p.reject(arg);
- p = new PromiseHandler();
- callback(p);
- }});
- }
- onStart(arg) { this._subscriber.start(arg); }
- onNext(arg) { this._subscriber.next(arg); }
- onError(arg) { this._subscriber.error(arg); }
- onComplete(arg) { this._subscriber.complete(arg);}
- disconnect() {
- if (!this._disconnectFunction) {
- return;
- }
- this._closed = true;
- this._disconnectFunction();
- delete this._disconnectFunction;
- this._subscriber;
- this._handlers.clear();
- }
- [observableSymbol]() {
- return this;
- }
- subscribe(onNext = null, onError = null, onCompleted = null) {
- this._initSubscriber();
- const isNop = [onNext, onError, onCompleted].every(f => f === null);
- const subscriber = Subscriber.create(onNext, onError, onCompleted);
- return this._subscribe({subscriber, isNop});
- }
- _subscribe({subscriber, isNop}) {
- if (!isNop && !this._disconnectFunction) {
- this._disconnectFunction = this._subscriberFunction(this._subscriber);
- }
- !isNop && this._handlers.add(subscriber);
- return new Subscription({
- observable: this,
- subscriber,
- unsubscribe: () => {
- if (isNop) { return; }
- this._handlers.remove(subscriber);
- if (this._handlers.isEmpty) {
- this.disconnect();
- }
- },
- closed: () => this.closed
- });
- }
- }
- Observable.observavle = observableSymbol;
- return Observable;
- })();
- const WindowResizeObserver = Observable.fromEvent(window, 'resize')
- .map(o => { return {width: window.innerWidth, height: window.innerHeight}; });
- const bounce = {
- origin: Symbol('origin'),
- raf(func) {
- let reqId = null;
- let lastArgs = null;
- const callback = () => {
- func(...lastArgs);
- reqId = lastArgs = null;
- };
- const result = (...args) => {
- if (reqId) {
- cancelAnimationFrame(reqId);
- }
- lastArgs = args;
- reqId = requestAnimationFrame(callback);
- };
- result[this.origin] = func;
- return result;
- },
- idle(func, time) {
- let reqId = null;
- let lastArgs = null;
- const [caller, canceller] =
- (time === undefined && window.requestIdleCallback) ?
- [window.requestIdleCallback, window.cancelIdleCallback] : [window.setTimeout, window.clearTimeout];
- const callback = () => {
- reqId = null;
- func(...lastArgs);
- lastArgs = null;
- };
- const result = (...args) => {
- if (reqId) {
- reqId = canceller(reqId);
- }
- lastArgs = args;
- reqId = caller(callback, time);
- };
- result[this.origin] = func;
- return result;
- },
- time(func, time = 0) {
- return this.idle(func, time);
- }
- };
- class DataStorage {
- static create(defaultData, options = {}) {
- return new DataStorage(defaultData, options);
- }
- constructor(defaultData, options = {}) {
- this._default = defaultData;
- this._data = Object.assign({}, defaultData);
- this.prefix = `${options.prefix || 'DATA'}_`;
- this.storage = options.storage || localStorage;
- this._ignoreExportKeys = options.ignoreExportKeys || [];
- this.readonly = options.readonly;
- this.silently = false;
- this._changed = new Map();
- this._onChange = bounce.idle(this._onChange.bind(this));
- objUtil.bridge(this, new Emitter());
- this.restore();
- this.props = this._makeProps(defaultData);
- }
- _makeProps(defaultData = {}, namespace = '') {
- namespace = namespace ? `${namespace}.` : '';
- const self = this;
- const def = {};
- const props = {};
- Object.keys(defaultData).sort()
- .filter(key => key.includes(namespace))
- .forEach(key => {
- const k = key.slice(namespace.length);
- if (k.includes('.')) {
- const ns = k.slice(0, k.indexOf('.'));
- props[ns] = this._makeProps(defaultData, `${namespace}${ns}`);
- }
- def[k] = {
- enumerable: !this._ignoreExportKeys.includes(key),
- get() { return self.getValue(key); },
- set(v) { self.setValue(key, v); }
- };
- });
- Object.defineProperties(props, def);
- return props;
- }
- _onChange() {
- const changed = this._changed;
- this.emit('change', changed);
- for (const [key, val] of changed) {
- this.emitAsync('update', key, val);
- this.emitAsync(`update-${key}`, val);
- }
- this._changed.clear();
- }
- onkey(key, callback) {
- this.on(`update-${key}`, callback);
- }
- offkey(key, callback) {
- this.off(`update-${key}`, callback);
- }
- restore(storage) {
- storage = storage || this.storage;
- Object.keys(this._default).forEach(key => {
- const storageKey = this.getStorageKey(key);
- if (storage.hasOwnProperty(storageKey) || storage[storageKey] !== undefined) {
- try {
- this._data[key] = JSON.parse(storage[storageKey]);
- } catch (e) {
- window.console.error('config parse error key:"%s" value:"%s" ', key, storage[storageKey], e);
- }
- } else {
- this._data[key] = this._default[key];
- }
- });
- }
- getNativeKey(key) {
- return key;
- }
- getStorageKey(key) {
- return `${this.prefix}${key}`;
- }
- refresh(key, storage) {
- storage = storage || this.storage;
- key = this.getNativeKey(key);
- const storageKey = this.getStorageKey(key);
- if (storage.hasOwnProperty(storageKey) || storage[storageKey] !== undefined) {
- try {
- this._data[key] = JSON.parse(storage[storageKey]);
- } catch (e) {
- window.console.error('config parse error key:"%s" value:"%s" ', key, storage[storageKey], e);
- }
- }
- return this._data[key];
- }
- getValue(key, refresh) {
- if (refresh) {
- return this.refresh(key);
- }
- key = this.getNativeKey(key);
- return this._data[key];
- }
- setValue(key, value) {
- const _key = key;
- key = this.getNativeKey(key);
- if (this._data[key] === value || arguments.length < 2) {
- return;
- }
- const storageKey = this.getStorageKey(key);
- const storage = this.storage;
- if (!this.readonly && storage[StorageWriter.writer]) {
- storage[StorageWriter.writer](storageKey, value);
- } else if (!this.readonly) {
- try {
- storage[storageKey] = JSON.stringify(value);
- } catch (e) {
- window.console.error(e);
- }
- }
- this._data[key] = value;
- if (!this.silently) {
- this._changed.set(_key, value);
- this._onChange();
- }
- }
- setValueSilently(key, value) {
- const isSilent = this.silently;
- this.silently = true;
- this.setValue(key, value);
- this.silently = isSilent;
- }
- export(isAll = false) {
- const result = {};
- const _default = this._default;
- Object.keys(this.props)
- .filter(key => isAll || (_default[key] !== this._data[key]))
- .forEach(key => result[key] = this.getValue(key));
- return result;
- }
- exportJson() {
- return JSON.stringify(this.export(), null, 2);
- }
- import(data) {
- Object.keys(this.props)
- .forEach(key => {
- console.log('import data: %s=%s', key, data[key]);
- this.setValueSilently(key, data[key]);
- });
- }
- importJson(json) {
- this.import(JSON.parse(json));
- }
- getKeys() {
- return Object.keys(this.props);
- }
- clear() {
- this.silently = true;
- const storage = this.storage;
- Object.keys(this._default)
- .filter(key => !this._ignoreExportKeys.includes(key)).forEach(key => {
- const storageKey = this.getStorageKey(key);
- try {
- if (storage.hasOwnProperty(storageKey) || storage[storageKey] !== undefined) {
- delete storage[storageKey];
- }
- this._data[key] = this._default[key];
- } catch (e) {}
- });
- this.silently = false;
- }
- namespace(name) {
- const namespace = name ? `${name}.` : '';
- const origin = Symbol(`${namespace}`);
- const result = {
- getValue: key => this.getValue(`${namespace}${key}`),
- setValue: (key, value) => this.setValue(`${namespace}${key}`, value),
- on: (key, func) => {
- if (key === 'update') {
- const onUpdate = (key, value) => {
- if (key.startsWith(namespace)) {
- func(key.slice(namespace.length + 1), value);
- }
- };
- onUpdate[origin] = func;
- this.on('update', onUpdate);
- return result;
- }
- return this.onkey(`${namespace}${key}`, func);
- },
- off: (key, func) => {
- if (key === 'update') {
- func = func[origin] || func;
- this.off('update', func);
- return result;
- }
- return this.offkey(`${namespace}${key}`, func);
- },
- onkey: (key, func) => {
- this.on(`update-${namespace}${key}`, func);
- return result;
- },
- offkey: (key, func) => {
- this.off(`update-${namespace}${key}`, func);
- return result;
- },
- props: this.props[name],
- refresh: () => this.refresh(),
- subscribe: subscriber => {
- return this.subscribe(subscriber)
- .filter(changed => changed.keys().some(k => k.startsWith(namespace)))
- .map(changed => {
- const result = new Map;
- for (const k of changed.keys()) {
- k.startsWith(namespace) && result.set(k, changed.get(k));
- }
- return result;
- });
- }
- };
- return result;
- }
- subscribe(subscriber) {
- subscriber = subscriber || {
- next: (...args) => window.console.log('next', ...args),
- error: (...args) => window.console.warn('error', ...args),
- complete: (...args) => window.console.log('complete', ...args)
- };
- const observable = new Observable(o => {
- const onChange = changed => o.next(changed);
- this.on('change', onChange);
- return () => this.off(onChange);
- });
- return observable.subscribe(subscriber);
- }
- }
- // already required /Users/kunio/Dropbox/github/ZenzaWatch/packages/lib/src/infra/bounce.js
- const config = (() => {
- const DEFAULT_CONFIG = {
- debug: false,
- 'videoInfo.openNewWindow': false,
- 'mylist.enableAutoComment': true, // マイリストコメントに投稿者を入れる
- 'responsive.matrix': false,
- 'nicoad.hide': false,
- 'ng.enable': false,
- 'ng.owner': '',
- 'ng.word': '',
- 'ng.tag': '',
- 'ng.syncZenza': false,
- 'fav.owner': '',
- 'fav.word': '',
- 'fav.tag': ''
- };
- return new DataStorage(
- DEFAULT_CONFIG,
- {
- prefix: `${PRODUCT}_config`,
- ignoreExportKeys: [],
- readonly: !location || location.host !== 'www.nicovideo.jp',
- storage: localStorage
- }
- );
- })();
- MylistPocket.broadcast = (function(config) {
- if (!window.BroadcastChannel) { return; }
- const broadcastChannel = new window.BroadcastChannel(PRODUCT);
- const onBroadcastMessage = (e) => {
- const data = e.data;
- switch (data.type) {
- case 'config-update':
- config.refresh(true);
- break;
- }
- };
- broadcastChannel.addEventListener('message', onBroadcastMessage);
- return {
- postMessage: (...args) => { broadcastChannel.postMessage(...args); }
- };
- })(config);
- config.on('update', (key, value) => {
- if (!config.props.hasOwnProperty(key)) { return; }
- MylistPocket.broadcast.postMessage(
- {type: 'config-update', key, value, storage: 'local'}
- );
- });
- MylistPocket.config = config;
- const CacheStorage = (function() {
- let 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];
- }
- });
- this.gc = bounce.time(this.gc.bind(this), 100);
- }
- gc(now = -1) {
- const storage = this._storage;
- now = now >= 0 ? now : Date.now();
- Object.keys(storage).forEach((key, index) => {
- if (key.indexOf(PREFIX) === 0) {
- let item;
- try {
- item = JSON.parse(this._storage[key]);
- } catch(e) {
- storage.removeItem(key);
- }
- //console.info(
- // `${index}, key: ${key}, expiredAt: ${new Date(item.expiredAt).toLocaleString()}, now: ${new Date(now).toLocaleString()}`);
- if (item.expiredAt === '' || item.expiredAt > 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;
- try {
- this._storage[key] = JSON.stringify(cacheData);
- this.gc();
- } catch (e) {
- if (e.name === 'QuotaExceededError' ||
- e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
- this.gc(0);
- }
- }
- }
- getItem(key) {
- key = PREFIX + key;
- if (!(this._storage.hasOwnProperty(key) || this._storage[key] !== undefined)) {
- 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[key] !== undefined) {
- 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 {
- let 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;
- })();
- class CrossDomainGate extends Emitter {
- static get hostReg() {
- return /^[a-z0-9]*\.nicovideo\.jp$/;
- }
- constructor(...args) {
- super();
- this.initialize(...args);
- }
- initialize(params) {
- this._baseUrl = params.baseUrl;
- this._origin = params.origin || location.href;
- this._type = params.type;
- this._suffix = params.suffix || '';
- this.name = params.name || params.type;
- this._sessions = {};
- this._initializeStatus = 'none';
- }
- _initializeFrame() {
- if (this._initializeStatus !== 'none') {
- return this.promise('initialize');
- }
- this._initializeStatus = 'initializing';
- setTimeout(() => {
- if (this._initializeStatus === 'done') {
- return;
- }
- this.emitReject('initialize', {
- status: 'timeout', message: `CrossDomainGate初期化タイムアウト (${this._type})`
- });
- }, 60 * 1000);
- this._initializeCrossDomainGate();
- return this.promise('initialize');
- }
- _initializeCrossDomainGate() {
- const loaderFrame = document.createElement('iframe');
- loaderFrame.referrerPolicy = 'origin';
- loaderFrame.sandbox = 'allow-scripts allow-same-origin';
- loaderFrame.loading = 'eager';
- loaderFrame.name = `${this._type}${PRODUCT}Loader${this._suffix ? `#${this._suffix}` : ''}`;
- loaderFrame.className = `xDomainLoaderFrame ${this._type}`;
- loaderFrame.style.cssText = `
- position: fixed; left: -100vw; pointer-events: none;user-select: none;`;
- (document.body || document.documentElement).append(loaderFrame);
- this._loaderWindow = loaderFrame.contentWindow;
- const onInitialMessage = event => {
- if (event.source !== this._loaderWindow) {
- return;
- }
- window.removeEventListener('message', onInitialMessage);
- this._onMessage(event);
- };
- window.addEventListener('message', onInitialMessage);
- this._loaderWindow.location.replace(this._baseUrl + '#' + TOKEN);
- }
- _onMessage(event) {
- const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
- const {id, type, token, sessionId, body} = data;
- if (id !== PRODUCT || type !== this._type || token !== TOKEN) {
- window.console.warn('invalid token:',
- {id, PRODUCT, type, _type: this._type, token, TOKEN});
- return;
- }
- if (!this.port && body.command === 'initialized') {
- const port = this.port = event.ports[0];
- port.addEventListener('message', this._onMessage.bind(this));
- port.start();
- port.postMessage({body: {command: 'ok'}, token: TOKEN});
- }
- return this._onCommand(body, sessionId);
- }
- _onCommand({command, status, params}, sessionId = null) {
- switch (command) {
- case 'initialized':
- if (this._initializeStatus !== 'done') {
- this._initializeStatus = 'done';
- const originalBody = params;
- const result = this._onCommand(originalBody, sessionId);
- this.emitResolve('initialize', {status: 'ok'});
- return result;
- }
- break;
- case 'message':
- BroadcastEmitter.emitAsync('message', params, 'broadcast', sessionId);
- break;
- default: {
- const session = this._sessions[sessionId];
- if (!session) {
- return;
- }
- if (status === 'ok') {
- session.resolve(params);
- } else {
- session.reject({message: status || 'fail'});
- }
- delete this._sessions[sessionId];
- }
- break;
- }
- }
- load(url, options) {
- return this._postMessage({command: 'loadUrl', params: {url, options}});
- }
- videoCapture(src, sec) {
- return this._postMessage({command: 'videoCapture', params: {src, sec}})
- .then(result => Promise.resolve(result.params.dataUrl));
- }
- _fetch(url, options) {
- return this._postMessage({command: 'fetch', params: {url, options}});
- }
- async fetch(url, options = {}) {
- const result = await this._fetch(url, options);
- if (typeof result === 'string' || !result.buffer || !result.init || !result.headers) {
- return result;
- }
- const {buffer, init, headers} = result;
- const _headers = new Headers();
- (headers || []).forEach(a => _headers.append(...a));
- const _init = {
- status: init.status,
- statusText: init.statusText || '',
- headers: _headers
- };
- if (options._format === 'arraybuffer') {
- return {buffer, init: _init};
- }
- return new Response(buffer, _init);
- }
- async configBridge(config) {
- const keys = config.getKeys();
- this._config = config;
- const configData = await this._postMessage({
- command: 'dumpConfig',
- params: { keys, url: '', prefix: PRODUCT }
- });
- for (const key of Object.keys(configData)) {
- config.props[key] = configData[key];
- }
- if (!this.constructor.hostReg.test(location.host) &&
- !config.props.allowOtherDomain) {
- return;
- }
- config.on('update', (key, value) => {
- if (key === 'autoCloseFullScreen') {
- return;
- }
- this._postMessage({command: 'saveConfig', params: {key, value, prefix: PRODUCT}}, false);
- });
- }
- async _postMessage(body, usePromise = true) {
- await this._initializeFrame();
- const sessionId = 's:' + Math.random();
- const {params} = body;
- return this._sessions[sessionId] =
- new PromiseHandler((resolve, reject) => {
- try {
- this.port.postMessage({body, sessionId, token: TOKEN}, params.transfer);
- if (!usePromise) {
- delete this._sessions[sessionId];
- resolve();
- }
- } catch (error) {
- console.log('%cException!', 'background: red;', {error, body});
- delete this._sessions[sessionId];
- reject(error);
- }
- });
- }
- postMessage(body, promise = true) {
- return this._postMessage(body, promise);
- }
- sendMessage(body, usePromise = false) {
- return this._postMessage({command: 'message', params: body}, usePromise);
- }
- pushHistory(path, title) {
- return this._postMessage({command: 'pushHistory', params: {path, title}}, false);
- }
- async bridgeDb({name, ver, stores}) {
- await this._postMessage({command: 'bridge-db', params: {name, ver, stores}});
- return (command, data, storeName, transfer) => {
- const params = {data, name, storeName, transfer};
- return this._postMessage({command: 'bridge-db', params: {command, params}});
- };
- }
- }
- 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 = 'https://www.nicovideo.jp/mylist_add/video/sm9';
- const tokenReg = /NicoAPI\.token *= *["']([a-z0-9-]+)["'];/;
- let m;
- return fetch(url, { credentials: 'include', _format: 'text'})
- .then(res => res.text())
- .then(result => {
- if ((m = tokenReg.exec(result))) {
- const token = m[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 = 'https://ext.nicovideo.jp/';
- const MESSAGE_ORIGIN = 'https://ext.nicovideo.jp/';
- const CACHE_EXPIRE_TIME = 60 * 60 * 1000;
- //const CACHE_EXPIRE_TIME = 60 * 1000;
- let gate = null;
- let cacheStorage = new CacheStorage(sessionStorage, true);
- let failedResult = {};
- class ThumbInfoLoader {
- constructor() {
- this._emitter = new Emitter();
- gate = new CrossDomainGate({
- baseUrl: BASE_URL,
- origin: MESSAGE_ORIGIN,
- type: 'thumbInfo',
- messager: WindowMessageEmitter
- });
- }
- _onMessage(data, type) {
- if (type !== 'videoInfoLoader') { return; }
- const info = data.message;
- this.emit('load', info, 'THUMB_WATCH');
- }
- _parseXml(xmlText) {
- return parseThumbInfo(xmlText);
- }
- async load(watchId, options = {}) {
- const cacheKey = `thumbInfo_${watchId}`;
- const cache = cacheStorage.getItem(cacheKey);
- if (failedResult[`${watchId}`]) {
- return Promise.reject({data: failedResult[`${watchId}`], watchId});
- }
- if (cache) {
- return cache;
- }
- const thumbInfo =
- await gate.fetch(`${BASE_URL}api/getthumbinfo/${watchId}`, options)
- .catch(e => { return {status: 'fail', message: e.message || `gate.fetch('${watchId}') failed` }; });
- thumbInfo.fromCache = !!cache;
- if (thumbInfo.status !== 'ok') {
- failedResult[`${watchId}`] = thumbInfo;
- return Promise.reject(thumbInfo);
- }
- cacheStorage.setItem(cacheKey, thumbInfo, CACHE_EXPIRE_TIME);
- return thumbInfo;
- }
- }
- const loader = new ThumbInfoLoader();
- return {
- load: watchId => loader.load(watchId),
- loadOwnerInfo: async watchId => {
- const info = await loader.load(watchId);
- const owner = info.owner;
- if (!owner) {
- return {};
- }
- const lang = util.getPageLanguage();
- const prefix = owner.type === 'user' ? '投稿者: ' : '提供: ';
- const suffix =
- (owner.type === 'user' && lang === 'ja-JP') ? ' さん' : '';
- owner.linkId =
- owner.id ?
- (owner.type === 'user' ? `user/${owner.id}` : `ch${owner.id}`) :
- '';
- 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 = 'https://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 (let i = 0, len = items.length; i < len; i++) {
- let 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 = 'https://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 = 'https://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} ${owner.linkId}`;
- return DeflistApiLoader.addItem(watchId, description);
- }, () => DeflistApiLoader.addItem(watchId));
- }
- 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));
- this._view.addEventListener('mousedown', this._onMousedown.bind(this));
- this._view.addEventListener('contextmenu', this._onContextMenu.bind(this));
- this._onHoverEnd = bounce.time(this._onHoverEnd.bind(this), 500);
- document.body.addEventListener(
- 'mouseover', this._onHover.bind(this), {passive: true});
- document.body.addEventListener(
- 'mouseout', this._onMouseout.bind(this), {passive: true});
- document.body.addEventListener(
- 'mouseover', this._onHoverEnd, {passive: true});
- document.body.addEventListener(
- 'click', () => { this.hide(); }, {passive: true});
- 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', bounce.time(() => {
- this.hide();
- }, 1000));
- });
- this.toggleClass('is-otherDomain', location.host !== 'www.nicovideo.jp');
- this.toggleClass('is-guest', !util.isLogin());
- 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) {
- e.preventDefault();
- e.stopPropagation();
- }
- _onContextMenu(e) {
- e.preventDefault();
- e.stopPropagation();
- }
- _onMousedown(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 if (command === 'playlist-queue') {
- this.emit('playlist-queue', watchId, this);
- } else {
- if (e.button !== 0 || e.shiftKey) {
- this._deflistRemove(watchId);
- } else {
- this._deflist(watchId);
- }
- }
- }
- _videoInfo(watchId) {
- this.emit('info', watchId || this._watchId, this);
- }
- _deflist(watchId) {
- this.emit('deflist-add', watchId || this._watchId, this);
- }
- _deflistRemove(watchId) {
- this.emit('deflist-remove', watchId || this._watchId, this);
- }
- _onHover(e) {
- const target = this._isTargetElement(e);
- if (!target) { return; }
- this._hoverElement = target;
- }
- _onHoverEnd(e) {
- const target =
- e.target.tagName === 'A' ? e.target : e.target.closest('a');
- if (!target || this._hoverElement !== target) { return; }
- const href = target.getAttribute('data-href') || target.getAttribute('href');
- const watchId = target.dataset.nicoVideoId || util.getWatchId(href);
- const offset = target.getBoundingClientRect();
- //const bodyOffset = document.body.getBoundingClientRect();
- const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
- const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0;
- const left = offset.left + scrollLeft;
- const top = offset.top + scrollTop;
- const host = target.hostname;
- if (host !== 'www.nicovideo.jp' && host !== 'nico.ms' && host !== 'sp.nicovideo.jp') { return; }
- if (target.classList.contains('noHoverMenu')) { return; }
- if (!watchId || !watchId.match(/^[a-z0-9]+$/)) { return; }
- if (watchId.indexOf('lv') === 0) { return; }
- this._watchId = watchId;
- this.show();
- this.moveTo(
- left + target.offsetWidth - this._view.offsetWidth / 2,
- top + target.offsetHeight / 2 - this._view.offsetHeight / 2
- );
- }
- _onMouseout(e) {
- const target = this._isTargetElement(e);
- if (!target) { return; }
- if (this._hoverElement === e.target) {
- this._hoverElement = null;
- }
- }
- _isTargetElement(e) {
- const target =
- e.target.tagName === 'A' ? e.target : e.target.closest('a');
- if (!target) { return false; }
- const href = target.href || '';
- if (!/(watch\/[a-z0-9]+|nico\.ms\/[a-z0-9]+)/.test(href)) { return false; }
- return target;
- }
- 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);
- this._deflistButton.setAttribute('data-result', result.message || '登録しました');
- this.removeClass('is-deflistUpdating');
- }
- notifyFailDeflistUpdate(result) {
- this.addClass('is-deflistFail');
- window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000);
- 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 = {};
- this._baseConfig = config;
- this._config = config.namespace('videoInfo');
- this._mylistConfig = config.namespace('mylist');
- const ngConfig = this._ngConfig = config.namespace('ng');
- const favConfig = this._favConfig = config.namespace('fav');
- this._nicoadConfig = config.namespace('nicoad');
- const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
- this._ngChecker = ngChecker;
- this._favChecker = favChecker;
- }
- _initialize() {
- if (this._isInitialized) { return; }
- const host = this._host;
- const tpl = this._tpl;
- this._shadowRoot = util.attachShadowDom({host, tpl});
- Array.prototype.forEach.call(this._host.querySelectorAll('*'), (elm) => {
- //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._shadowRoot.addEventListener('mousedown', e => { e.stopPropagation(); });
- this._rootDom.querySelector('.setting-panel-main').addEventListener('click', e => {
- e.stopPropagation();
- });
- this._initSettingPanel();
- const updateNgEnable = v => { this.toggleClass('is-ng-enable', v); };
- updateNgEnable(this._ngConfig.props.enable);
- this._ngConfig.onkey('enable', updateNgEnable);
- this._rootDom.addEventListener('click', this._onClick.bind(this));
- this._boundOnBodyMouseDown = this._onBodyMouseDown.bind(this);
- MylistPocket.debug.view = this;
- util.emitter.on('hideHover', () => {
- this.hide();
- });
- const debUpdateFavNg = bounce.time(this._updateFavNg.bind(this), 100);
- this._ngConfig .on('update', debUpdateFavNg);
- this._favConfig .on('update', debUpdateFavNg);
- //this._mylistConfig.on('update', debUpdateFavNg);
- ZenzaDetector.detect().then(() => {
- this._isZenzaReady = true;
- this.addClass('is-zenzaReady');
- window.ZenzaWatch.emitter.on('DialogPlayerOpen', bounce.time(() => {
- 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.toggleClass('is-firefox', util.isFirefox());
- MylistPocket.external.observe({
- query: 'a.videoLink',
- container: this._hostDom.querySelector('.description'),
- });
- this._isInitialized = true;
- }
- _initSettingPanel() {
- const onSettingFormChange = this._onSettingFormChange.bind(this);
- const refresh = () => {
- Array.from(this._rootDom.querySelectorAll('.setting-form')).forEach(elm => {
- const name = elm.getAttribute('data-config-name');
- if (!name) { return; }
- const namespace = elm.getAttribute('data-config-namespace') || '';
- let config = this._config;
- switch (namespace) {
- case 'ng':
- config = this._ngConfig;
- break;
- case 'fav':
- config = this._favConfig;
- break;
- case 'mylist':
- config = this._mylistConfig;
- break;
- case 'nicoad':
- config = this._nicoadConfig;
- break;
- default:
- config = this._baseConfig;
- }
- const tagName = (elm.tagName.toLowerCase()).toLowerCase();
- if (tagName === 'input') {
- const type = (elm.type || '').toLowerCase();
- switch (type) {
- case 'checkbox':
- elm.checked = !!config.props[name];
- break;
- default:
- elm.value = config.props[name];
- break;
- }
- } else if (tagName === 'select' || tagName === 'textarea') {
- elm.value = config.props[name];
- }
- elm.removeEventListener('change', onSettingFormChange);
- elm.addEventListener('change', onSettingFormChange);
- });
- };
- const onUpdate = bounce.time(refresh, 100);
- const syncZenza = bounce.time(() => {
- if (!this._ngConfig.props.syncZenza || !this._isZenzaReady) { return; }
- window.ZenzaWatch.config.setValue('videoTagFilter', this._ngConfig.props.tag);
- window.ZenzaWatch.config.setValue('videoOwnerFilter', this._ngConfig.props.owner);
- }, 1000);
- refresh();
- this._config.on('update', onUpdate);
- this._favConfig.on('update', onUpdate);
- this._ngConfig.on('update', () => {
- onUpdate();
- syncZenza();
- });
- }
- _onSettingFormChange(e) {
- const elm = e.target;
- const name = elm.getAttribute('data-config-name');
- if (!name) { return; }
- const namespace = elm.getAttribute('data-config-namespace') || '';
- let config = this._config;
- switch (namespace) {
- case 'ng':
- config = this._ngConfig;
- break;
- case 'fav':
- config = this._favConfig;
- break;
- case 'mylist':
- config = this._mylistConfig;
- break;
- case 'nicoad':
- config = this._nicoadConfig;
- break;
- default:
- config = this._baseConfig;
- }
- const tagName = (elm.tagName.toLowerCase()).toLowerCase();
- if (tagName === 'input') {
- const type = (elm.type || '').toLowerCase();
- switch (type) {
- case 'checkbox':
- config.props[name] = elm.checked;
- break;
- default:
- config.props[name] = elm.value;
- break;
- }
- } else if (tagName === 'select' || tagName === 'textarea') {
- config.props[name] = elm.value;
- }
- }
- 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) {
- this._videoInfo = videoInfo;
- 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');
- switch (command) {
- case 'toggle-setting':
- this.toggleSettingPanel();
- break;
- case 'add-ng-tag': case 'add-fav-tag':
- case 'toggle-ng-tag': case 'toggle-fav-tag': {
- const tag = elm.getAttribute('data-tag') || '';
- if (!tag) { break; }
- this.emit('command', command, {
- watchId: this._videoInfo.watchId,
- value: tag
- }, this);
- }
- break;
- case 'add-ng-owner': case 'add-fav-owner':
- case 'toggle-ng-owner': case 'toggle-fav-owner': {
- let owner =
- (this._videoInfo.isChannel ? 'ch' : '') +
- this._videoInfo.ownerId + '#' + this._videoInfo.ownerName;
- this.emit('command', command, {
- watchId: this._videoInfo.watchId,
- value: owner
- }, this);
- }
- break;
- case 'mylist-comment-open':
- this.emit('command', command, this._videoInfo.watchId);
- break;
- case 'close':
- this.hide();
- break;
- default:
- this.emit('command', command, param, this);
- }
- }
- _updateFavNg() {
- if (!this._isInitialized) { return; }
- if (!this._videoInfo || this._videoInfo.status !== 'ok') { return; }
- const videoInfo = this._videoInfo;
- const ownerInfo = this._rootDom.querySelector('.owner-info');
- ownerInfo.classList.toggle('is-favorited',
- this._favChecker.isMatchOwner(videoInfo.owner));
- ownerInfo.classList.toggle('is-ng',
- this._ngChecker .isMatchOwner(videoInfo.owner));
- Array.prototype.forEach.call(
- this._rootDom.querySelectorAll('.tag-container'),
- (elm) => {
- const tag = elm.getAttribute('data-tag');
- elm.classList.toggle('is-favorited', this._favChecker.isMatchTag(tag));
- elm.classList.toggle('is-ng', this._ngChecker.isMatchTag(tag));
- });
- }
- toggleSettingPanel() {
- this.toggleClass('is-setting');
- }
- _onBodyMouseDown() {
- document.body.removeEventListener('mousedown', this._boundOnBodyMouseDown);
- this.hide();
- }
- reset() {
- this._initialize();
- window.setTimeout(() => { this._videoInfoArea.scrollTop = 0; }, 0);
- this.removeClass('noclip');
- this.addClass('is-loading');
- }
- show() {
- this.addClass('show');
- document.body.addEventListener('mousedown', this._boundOnBodyMouseDown);
- }
- hide() {
- this._videoInfoArea.scrollTop = 0;
- this.removeClass('show is-ok is-fail noclip is-setting');
- }
- _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];
- 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.replace('http:', 'https:');
- break;
- case 'date':
- elm.textContent = data.toLocaleString();
- break;
- default:
- elm.textContent = data;
- }
- });
- const df = document.createDocumentFragment();
- //Array.prototype.forEach.call(this._host.querySelectorAll('.tag'), t => { t.remove(); });
- videoInfo.tags.forEach(tag => {
- df.appendChild((this._createTagSlot(tag, videoInfo)));
- });
- const videoTags = this._rootDom.querySelector('.video-tags');
- videoTags.innerHTML = '';
- videoTags.appendChild(df);
- Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-watch-id'), elm => {
- elm.setAttribute('data-param', videoInfo.watchId);
- });
- Array.prototype.forEach.call(this._rootDom.querySelectorAll('.command-video-id'), elm => {
- elm.setAttribute('data-param', videoInfo.videoId);
- });
- const target = this._config.props.openNewWindow ? '_blank' : '_self';
- Array.prototype.forEach.call(
- this._host.querySelectorAll('.target-change'), elm => {
- elm.target = target;
- elm.rel = 'noopener';
- });
- this._updateFavNg();
- this.toggleClass('is-channel', videoInfo.isChannel);
- this.addClass('is-ok');
- this.removeClass('is-fail');
- window.setTimeout(() => { this.addClass('noclip'); }, 800);
- }
- _createDescription(elm, data) {
- elm.innerHTML = util.httpLink(data);
- const watchReg = /watch\/([a-z0-9]+)/;
- const isZenzaReady = this._isZenzaReady;
- //if (util.isFirefox()) { return; }
- Array.from(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 label = document.createElement('span');
- label.className = 'label';
- label.textContent = link.textContent;
- link.textContent = '';
- link.append(label);
- 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);
- const img = document.createElement('img');
- img.className = 'videoThumbnail preview';
- img.src = 'https://nicovideo.cdn.nimg.jp/uni/img/common/video_deleted.jpg';//(thumbnail || '').replace(/^http:/, '');
- link.classList.add('popupThumbnail');
- link.appendChild(img);
- link.dataset.videoId = watchId;
- link.classList.add('watch');
- });
- }
- _bindFail(videoInfo) {
- this._slot['error-description'].textContent =
- `動画情報の取得に失敗しました (${videoInfo.description})`;
- this.addClass('is-fail');
- this.removeClass('is-ok');
- }
- _createTagSlot(tag, {isChannel, owner}) {
- const text = util.escapeHtml(tag.text);
- const lock = tag.isLocked ? 'is-locked' : '';
- const span = document.createElement('span');
- const ownerId = owner ? owner.id : '';
- const a = document.createElement('a');
- const target = this._config.props.openNewWindow ? '_blank' : '_self';
- a.textContent = tag.text;
- a.className = `tag ${lock}`;
- a.target = target;
- a.rel = 'noopener';
- a.href = `https://www.nicovideo.jp/tag/${encodeURIComponent(text)}`;
- span.appendChild(a);
- if (isChannel) {
- const ch = document.createElement('a');
- const target = this._config.props.openNewWindow ? '_blank' : '_self';
- ch.textContent = '[ch]';
- ch.className = `tag ${lock} channel-search`;
- ch.target = target;
- ch.rel = 'noopener';
- ch.title = 'チャンネル検索';
- //ch.href = `http://ch.nicovideo.jp/search/${encodeURIComponent(text)}?channel_id=ch${ownerId}&type=video&mode=t`;
- ch.href = `https://ch.nicovideo.jp/search/${encodeURIComponent(text)}?type=video&mode=t`;
- span.appendChild(ch);
- }
- const fav = document.createElement('button');
- fav.className = 'add-fav-button command';
- fav.setAttribute('data-command', 'toggle-fav-tag');
- fav.setAttribute('data-tag', tag.text);
- fav.innerHTML = '★'; //'⃠'; // ✖
- span.appendChild(fav);
- const bt = document.createElement('button');
- bt.className = 'add-ng-button command';
- bt.setAttribute('data-command', 'toggle-ng-tag');
- bt.setAttribute('data-tag', tag.text);
- bt.innerHTML = '✖'; //'⃠'; // ✖
- span.appendChild(bt);
- const menu = `<zenza-tag-item-menu
- class="tagItemMenu"
- data-text="${encodeURIComponent(text)}"
- data-has-nicodic="0"
- ></zenza-tag-item-menu>`;
- span.insertAdjacentHTML('afterbegin', menu);
- span.className = 'tag-container';
- span.setAttribute('data-tag', tag.text);
- span.slot = 'tag';
- return span;
- }
- notifyBeginDeflistUpdate(/*watchId*/) {
- this.addClass('is-deflistUpdating');
- }
- notifyEndDeflistUpdate(result) {
- this.addClass('is-deflistSuccess');
- window.setTimeout(() => { this.removeClass('is-deflistSuccess'); }, 3000);
- this._deflistButton.setAttribute('data-result', result.message || '登録しました');
- this.removeClass('is-deflistUpdating');
- }
- notifyFailDeflistUpdate(result) {
- this.addClass('is-deflistFail');
- window.setTimeout(() => { this.removeClass('is-deflistFail'); }, 3000);
- this._deflistButton.setAttribute('data-result', result.message || '登録失敗');
- this.removeClass('is-deflistUpdating');
- }
- }
- class VideoInfo {
- static createByThumbInfo(thumbInfo) {
- let thumbnail = thumbInfo.thumbnail;
- if (util.hasLargeThumbnail(thumbInfo.videoId)) {
- thumbnail = thumbnail.replace(/\.[ML]$/) + '.L';
- }
- const owner = thumbInfo.owner || {};
- const isChannel = thumbInfo.isChannel;
- const rawData = {
- status: thumbInfo.status,
- videoId: thumbInfo.id,
- watchId: thumbInfo.v,
- videoTitle: thumbInfo.title,
- videoThumbnail: thumbnail,
- uploadDate: thumbInfo.postedAt,
- duration: textUtil.secToTime(thumbInfo.duration),
- viewCounter: thumbInfo.viewCount,
- mylistCounter: thumbInfo.mylistCount,
- commentCounter: thumbInfo.commentCount,
- description: thumbInfo.description,
- lastResBody: thumbInfo.lastResBody,
- isChannel,
- ownerId: owner.id,
- ownerName: owner.name,
- ownerIcon: owner.icon,
- tags: thumbInfo.tagList.map(tag => { return {text: tag.text, isLocked: tag.lock}; })
- };
- 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 originalVideoId() {
- return (!this.isChannel && this.videoId !== this.watchId) ? this.videoId : '';
- }
- 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 owner() {
- return {
- type: this.isChannel ? 'channel' : 'user',
- id: this.ownerId,
- linkId: this.ownerId ? (this.isChannel ? `ch${this.ownerId}` : `user/${this.ownerId}`) : 'xx',
- name: this.ownerName,
- icon: this.ownerIcon
- };
- }
- get ownerPageLink() {
- const ownerId = this.ownerId;
- if (this.isChannel) {
- return `${protocol}//ch.nicovideo.jp/ch${ownerId}`;
- } else {
- return `${protocol}//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) => {
- const enableAutoComment = config.props.mylist.enableAutoComment;
- if (location.host === 'www.nicovideo.jp') {
- if (enableAutoComment) {
- return DeflistApiLoader.addItemWithOwnerName(watchId);
- } else {
- return DeflistApiLoader.addItem(watchId, '');
- }
- }
- let zenza;
- let token;
- return ZenzaDetector.detect().then((z) => {
- zenza = z;
- }).then(() => {
- return CsrfTokenLoader.load().then((t) => {
- token = t;
- }, () => { return Promise.resolve(); });
- }).then(() => {
- if (!enableAutoComment) { return {}; }
- return ThumbInfoLoader.loadOwnerInfo(watchId);
- }).then((owner) => {
- if (!owner.id) {
- return zenza.external.deflistAdd({watchId});
- }
- const description = `${owner.localeName} ${owner.linkId}`;
- return zenza.external.deflistAdd({watchId, description, token});
- });
- };
- const deflistRemove = (watchId) => {
- if (location.host === 'www.nicovideo.jp') {
- return DeflistApiLoader.removeItem(watchId);
- }
- let zenza;
- let token;
- return ZenzaDetector.detect().then((z) => {
- zenza = z;
- }).then(() => {
- return CsrfTokenLoader.load().then((t) => {
- token = t;
- }, () => { return Promise.resolve(); });
- }).then(() => {
- return zenza.external.deflistRemove({watchId, token});
- });
- };
- class MatchChecker {
- constructor({word = '', tag = '', owner = ''}) {
- this.init({word, tag, owner});
- }
- init({word, tag, owner}) {
- this._tag = [];
- tag.split(/[\r\n]+/).forEach((t) => {
- if (t) { this._tag.push(t.trim()); }
- });
- this._tag = _.uniq(this._tag);
- let wordTmp = [];
- this._word = null;
- word.split(/[\r\n]+/).forEach((w) => {
- if (w) { wordTmp.push(util.escapeRegs(w.trim())); }
- });
- wordTmp = _.uniq(wordTmp);
- if (wordTmp.length > 0) {
- this._word = new RegExp('(' + wordTmp.join('|') + ')', 'i');
- }
- this._userId = [];
- this._channelId = [];
- owner.split(/[\r\n]+/).forEach((o) => {
- if (typeof o === 'string') {
- const id = o.split('#')[0].trim();
- if (id.startsWith('ch')) {
- this._channelId.push(parseInt(id.substring(2)));
- } else {
- this._userId.push(parseInt(id));
- }
- }
- });
- this._userId = _.uniq(this._userId);
- this._channelId = _.uniq(this._channelId);
- }
- isMatch(data) {
- if (this._isMatchTag(data.tagList)) { return true; }
- if (this._isMatchOwner(data.owner)) { return true; }
- if (this._isMatchWord({title: data.title, description: data.description})) { return true; }
- }
- _isMatchTag(tagList = []) {
- if (this._tag.length < 1) { return false; }
- const tagTmp = [];
- tagList.forEach(t => { if (t) { tagTmp.push(util.escapeRegs(t.trim ? t.trim() : t.text.trim())); } });
- const tagReg = new RegExp(' (' + tagTmp.join('|') + ') ', 'i');
- const _tag = ' ' + this._tag.join(' ') + ' ';
- return tagReg.test(_tag);
- }
- _isMatchOwner(owner) {
- const _id = owner.type === 'user' ? this._userId : this._channelId;
- return _id.includes(parseInt(owner.id, 10));
- }
- _isMatchWord({title, description}) {
- if (!this._word) { return false; }
- return this._word.test(title) || this._word.test(description);
- }
- isMatchTag(tag) {
- return this._isMatchTag([tag]);
- }
- isMatchOwner(owner) {
- return this._isMatchOwner(owner);
- }
- }
- class NgChecker extends MatchChecker {
- isNg(data) {
- return super.isMatch(data);
- }
- }
- 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 = thumbInfo => {
- const vi = VideoInfo.createByThumbInfo(thumbInfo);
- vv.bind(vi);
- };
- const onVideoInfoFail = () => {
- vv.bind({status: 'fail', description: '通信失敗'});
- return Promise.resolve();
- };
- return watchId => {
- vv.reset();
- vv.show();
- return ThumbInfoLoader.load(watchId, {expireTime: 60 * 60 * 1000}).then(onVideoInfoLoad, onVideoInfoFail);
- };
- };
- const createCommandDispatcher = ({infoView}) => {
- const info = createVideoInfoLoader(infoView);
- const ngConfig = config.namespace('ng');
- const favConfig = config.namespace('fav');
- const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
- const toggleFavNg = (command, param) => {
- let [cmd, namespace, key] = command.split('-');
- let _config = namespace === 'fav' ? favConfig : ngConfig;
- _config.refresh();
- const value = param.value.trim();
- let ngs = _config.props[key].trim().split(/[\r\n]/);
- const isContain = ngs.includes(value);
- if (isContain || cmd === 'remove') {
- ngs = ngs.filter((line) => {
- if (line === value) {
- window.console.info('%c-%s:%s', 'background: cyan', key, value);
- }
- return line !== value;
- });
- cmd = 'remove';
- } else if (!isContain || cmd === 'add') {
- ngs.push(value);
- window.console.info('%c+%s:%s', 'background: cyan', key, value);
- cmd = 'add';
- }
- ngs = _.uniq(ngs);
- _config.props[key] = ngs.join('\n').trim();
- const className = namespace === 'fav' ? 'is-fav-favorited' : 'is-ng-rejected';
- Array.prototype.forEach.call(
- document.querySelectorAll(`*[data-watch-id=${param.watchId}]`),
- item => { item.classList.toggle(className, cmd === 'add'); });
- };
- return (command, param, src) => {
- switch(command) {
- case 'info':
- return info(param);
- case 'load':
- return QueueLoader.load(param);
- case 'fav-status':
- return QueueLoader.load(param).then((result) => {
- if (!result || result.status === 'fail' || result.code === 'DELETED') {
- return Promise.reject({status: 'unknown', result});
- }
- if (ngChecker.isMatch(result)) {
- return {status: 'ng', result};
- }
- if (favChecker.isMatch(result)) {
- return {status: 'favorite', result};
- }
- return {status: 'default', result};
- });
- case 'mylist-window':
- window.open(
- protocol + '//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 'open-mylist-open':
- window.open(protocol + '//www.nicovideo.jp/openlist/' + param);
- break;
- case 'mylist-comment-open':
- window.open(protocol + '//www.nicovideo.jp/mylistcomment/video/' + param);
- break;
- case 'zenza-open-now':
- if (window.ZenzaWatch.config &&
- window.ZenzaWatch.config.getValue('enableSingleton')) {
- window.ZenzaWatch.external.sendOrExecCommand('openNow', param);
- } else {
- window.ZenzaWatch.external.execCommand('openNow', param);
- }
- break;
- case 'zenza-open':
- if (window.ZenzaWatch.config.getValue('enableSingleton')) {
- window.ZenzaWatch.external.sendOrOpen(param);
- } else {
- window.ZenzaWatch.external.open(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) => {
- src.notifyEndDeflistUpdate(result);
- }, (err) => {
- console.error('deflist-add-result', err);
- src.notifyFailDeflistUpdate(err);
- });
- case 'deflist-remove':
- src.notifyBeginDeflistUpdate('is-deflistUpdating');
- return deflistRemove(param)
- .then(util.getSleepPromise(1000, 'deflist-remove'))
- .then(() => {
- src.notifyEndDeflistUpdate({message: '削除しました'});
- }, (err) => {
- console.error('deflist-remove-result', err);
- src.notifyFailDeflistUpdate(err);
- });
- case 'add-ng-word': case 'add-ng-tag': case 'add-ng-owner':
- case 'add-fav-word': case 'add-fav-tag': case 'add-fav-owner':
- case 'remove-ng-word': case 'remove-ng-tag': case 'remove-ng-owner':
- case 'remove-fav-word': case 'remove-fav-tag': case 'remove-fav-owner':
- case 'toggle-ng-word': case 'toggle-ng-tag': case 'toggle-ng-owner':
- case 'toggle-fav-word': case 'toggle-fav-tag': case 'toggle-fav-owner':
- toggleFavNg(command, param);
- break;
- }
- };
- };
- const initExternal = (dispatcher, hoverMenu, infoView) => {
- MylistPocket.external = {
- info: watchId => { return dispatcher('info', watchId); },
- load: watchId => { return dispatcher('load', watchId, {expireTime: 60 * 60 * 1000}); },
- getFavStatus: (watchId) => { return dispatcher('fav-status', watchId); },
- observe: (params /*{query, container, closest}*/) => { initNg(params); },
- hide: () => {
- hoverMenu.hide();
- infoView.hide();
- }
- };
- MylistPocket.isReady = true;
- const ev = new CustomEvent('MylistPocketInitialized', { detail: { MylistPocket } });
- document.body.dispatchEvent(ev);
- // 過去の互換用
- if (window.jQuery) {
- window.jQuery('body').trigger('MylistPocketReady', MylistPocket);
- }
- };
- const QueueLoader = (() => {
- let lastPromise = null;
- let count = 0;
- const MAX_LOAD = 6;
- const promises = [];
- const load = function(watchId, item) {
- count = (count + 1) % MAX_LOAD;
- lastPromise = promises[count];
- const onLoad = info => {
- if (item) {
- watchId = info.watchId;
- item.setAttribute('data-watch-id', watchId);
- item.setAttribute('data-thumb-info', JSON.stringify(info));
- }
- const sleepTime = info.fromCache ? 0 : 50;
- return (util.getSleepPromise(sleepTime, 'success-' + watchId))(info);
- };
- const onFail = util.getSleepPromise(1000, 'fail-' + watchId);
- if (!lastPromise) {
- if (item) { item.classList.add('is-ng-current'); }
- lastPromise = ThumbInfoLoader.load(watchId).then(onLoad, onFail);
- } else {
- //lastPromise = Promise.all([lastPromise]).then(() => {
- lastPromise = Promise.race(promises).then(() => {
- if (item) { item.classList.add('is-ng-current'); }
- return ThumbInfoLoader.load(watchId).then(onLoad, onFail);
- });
- }
- promises[count] = lastPromise;
- return lastPromise;
- };
- return {
- load
- };
- })();
- const getNgEnv = () => {
- if (location.host === 'www.nicovideo.jp' &&
- (location.pathname.startsWith('/ranking') ||
- location.pathname.startsWith('/tag') ||
- location.pathname.startsWith('/search'))
- ) {
- return {
- query:
- '.item[data-video-id]:not(.is-ng-wait), .item_cell[data-video-id]:not(.is-ng-wait), .VideoItem:not(.is-ng-wait), .RankingMainVideo[data-video-id]:not(.is-ng-wait)',
- container:
- Array.from(
- document.querySelectorAll(
- '.contentBody .list, .container.column1024-0, .RankingMatrixVideosRow, .RankingMainContainer, .RankingVideoListContainer')
- )
- };
- }
- if (location.host === 'www.nicovideo.jp' &&
- document.querySelector('#MyPageNicorepoApp, #UserPageNicorepoApp')) {
- return {
- query: '.NicorepoTimelineItem:not(.is-ng-wait)',
- container: document.querySelector('#MyPageNicorepoApp, #UserPageNicorepoApp'),
- };
- }
- if (location.host === 'ch.nicovideo.jp' &&
- location.pathname.startsWith('/search')) {
- return {
- query: '.item:not(.is-ng-wait)',
- container: document.querySelector('.site_body')
- };
- }
- if (location.host === 'search.nicovideo.jp') {
- return {
- query: '.video:not(.is-ng-wait)',
- container: document.querySelector('#row-results')
- };
- }
- return {query: null, container: null};
- };
- const initNgConfig = () => {
- const ngConfig = config.namespace('ng');
- const updateEnable = v => { document.body.classList.toggle('is-ng-disable', !v); };
- updateEnable(ngConfig.props.enable);
- if (!ngConfig.props.enable) { return {}; }
- ngConfig.onkey('enable', updateEnable);
- const favConfig = config.namespace('fav');
- return {ngConfig, favConfig};
- };
- const initNgChecker = ({ngConfig, favConfig}) => {
- const ngChecker = new NgChecker({
- word: ngConfig.props.word,
- tag: ngConfig.props.tag,
- owner: ngConfig.props.owner
- });
- ngConfig.on('update', bounce.time(({key, value}) => {
- ngChecker.init({
- word: ngConfig.props.word,
- tag: ngConfig.props.tag,
- owner: ngConfig.props.owner
- });
- }, 100));
- const favChecker = new MatchChecker({
- word: favConfig.props.word,
- tag: favConfig.props.tag,
- owner: favConfig.props.owner
- });
- favConfig.on('update', bounce.time(({key, value}) => {
- favChecker.init({
- word: favConfig.props.word,
- tag: favConfig.props.tag,
- owner: favConfig.props.owner
- });
- }, 100));
- return {ngChecker, favChecker};
- };
- const initIntersectionObserver = onInview => {
- const onItemInview = item => {
- let watchId = item.getAttribute('data-id') ||
- item.getAttribute('data-video-id') ||
- item.getAttribute('data-watch-id');
- const ignore = () => item.classList.add('is-ng-ignore');
- if (!watchId) {
- const a = item.querySelector('a[href*=\'watch/\']');
- let m;
- if (!a) { return ignore(); }
- if (a.hostname !== 'www.nicovideo.jp') { return ignore(); }
- if ((m = /^\/watch\/([a-z0-9]+)/.exec(a.pathname)) === null) { return ignore(); }
- watchId = m[1];
- }
- if (!watchId) { return ignore(); }
- item.classList.add('is-ng-queue');
- onInview(item, watchId);
- };
- const intersectionObserver = new window.IntersectionObserver(entries => {
- entries.filter(entry => entry.isIntersecting).forEach(entry => {
- const item = entry.target;
- intersectionObserver.unobserve(item);
- onItemInview(item);
- });
- }, { rootMargin: '400px'});
- return intersectionObserver;
- };
- const initNgDom = ({intersectionObserver, query, closest, container}) => {
- if (!container) { return; }
- util.addStyle(__ng_css__);
- const update = container => {
- let items = (container || document).querySelectorAll(query);
- if (!items || items.length < 1) { return; }
- if (closest) {
- let tmp = [];
- Array.from(items).forEach(item => {
- const c = item.closest(closest);
- if (c && !tmp.includes(c)) {
- tmp.push(c);
- }
- });
- items = tmp;
- }
- if (!items || items.length < 1) { return; }
- Array.from(items).forEach(item => {
- //if (item.offsetLeft < 0) { return; }
- if (item.classList.contains('is-ng-ignore')) { return; }
- item.classList.add('is-ng-wait');
- intersectionObserver.observe(item);
- });
- };
- update();
- const onUpdate = _.throttle(update, 1000);
- if (!container) { return; }
- const mutationObserver = new MutationObserver(mutations => {
- if (mutations
- .some(mutation => mutation.addedNodes && mutation.addedNodes.length)) {
- onUpdate();
- }
- });
- const containers = Array.isArray(container) ? container : [container];
- containers.forEach(container => {
- mutationObserver.observe(
- container,
- {childList: true, characterData: false, attributes: false, subtree: false}
- );
- });
- };
- const initNg = params => {
- if (!window.IntersectionObserver) { return; }
- let {query, container, closest} = params ? params : getNgEnv();
- if (!query) { return; }
- const {ngConfig, favConfig} = initNgConfig();
- if (!ngConfig) { return; }
- const {ngChecker, favChecker} = initNgChecker({ngConfig, favConfig});
- const onItemInview = (item, watchId) => {
- const loadLazy = () => {
- const lazyImage = item.querySelector('.jsLazyImage');
- if (lazyImage) {
- const origImage = lazyImage.getAttribute('data-original');
- if (origImage) {
- lazyImage.src = origImage;
- lazyImage.classList.remove('jsLazyImage');
- }
- }
- };
- QueueLoader.load(watchId, item).then(
- info => {
- item.classList.remove('is-ng-current');
- if (!info || info.status === 'fail' || info.code === 'DELETED') {
- if (info && info.code !== 'COMMUNITY') {
- console.error('empty data', watchId, info, info ? info.code : 'unknown');
- }
- item.classList.add('is-ng-failed', info ? info.code : 'is-no-data');
- } else {
- item.classList.add(
- ngChecker.isNg(info) ? 'is-ng-rejected' : 'is-ng-resolved');
- if (favChecker.isMatch(info)) {
- item.classList.add('is-fav-favorited');
- }
- for (let img of item.querySelectorAll('img.videoThumbnail.preview')) {
- img.src = info.thumbnail;
- }
- let label = item.querySelector('.label');
- item.dataset.title = info.title;
- // チャンネル動画のリンクを watch/so〜 に置き換える
- if (!(info.id || '').startsWith('so')) { return; }
- if (label &&
- item.classList.contains('videoLink')
- ) {
- label.textContent = info.id;
- item.dataset.param = item.dataset.videoId = info.id;
- item.href = `https://www.nicovideo.jp/watch/${info.id}`;
- }
- for (let a of item.querySelectorAll(`a[href*="watch/${watchId}"]`)) {
- let href = a.getAttribute('href');
- href = href.replace(/watch\/([0-9]+)/, `watch/${info.id}`);
- a.setAttribute('href', href.replace(/^http:/, 'https:'));
- }
- }
- loadLazy();
- },
- () => {
- item.classList.remove('is-ng-current');
- item.classList.add('is-ng-failed');
- loadLazy();
- }
- );
- };
- const intersectionObserver = initIntersectionObserver(onItemInview);
- initNgDom({intersectionObserver, query, container, closest});
- return intersectionObserver;
- };
- 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);
- });
- hoverMenu.on('deflist-remove', (watchId, src) => {
- dispatcher('deflist-remove', watchId, src);
- });
- hoverMenu.on('playlist-queue', (watchId, src) => {
- dispatcher('playlist-queue', watchId, src);
- });
- MylistPocket.debug.hoverMenu = hoverMenu;
- initNg();
- if (config.props.nicoad.hide) {
- util.addStyle(nicoadHideCss);
- }
- if (document.body.classList.contains('MatrixRanking-body') &&
- config.props.responsive.matrix) {
- util.addStyle(responsiveCss);
- }
- initExternal(dispatcher, hoverMenu, infoView);
- };
- init();
- };
- function EmitterInitFunc() {
- class Handler { //extends Array {
- constructor(...args) {
- this._list = args;
- }
- get length() {
- return this._list.length;
- }
- exec(...args) {
- if (!this._list.length) {
- return;
- } else if (this._list.length === 1) {
- this._list[0](...args);
- return;
- }
- for (let i = this._list.length - 1; i >= 0; i--) {
- this._list[i](...args);
- }
- }
- execMethod(name, ...args) {
- if (!this._list.length) {
- return;
- } else if (this._list.length === 1) {
- this._list[0][name](...args);
- return;
- }
- for (let i = this._list.length - 1; i >= 0; i--) {
- this._list[i][name](...args);
- }
- }
- add(member) {
- if (this._list.includes(member)) {
- return this;
- }
- this._list.unshift(member);
- return this;
- }
- remove(member) {
- this._list = this._list.filter(m => m !== member);
- return this;
- }
- clear() {
- this._list.length = 0;
- return this;
- }
- get isEmpty() {
- return this._list.length < 1;
- }
- *[Symbol.iterator]() {
- const list = this._list || [];
- for (const member of list) {
- yield member;
- }
- }
- next() {
- return this[Symbol.iterator]();
- }
- }
- Handler.nop = () => {/* ( ˘ω˘ ) スヤァ */};
- const PromiseHandler = (() => {
- const id = function() { return `Promise${this.id++}`; }.bind({id: 0});
- class PromiseHandler extends Promise {
- constructor(callback = () => {}) {
- const key = new Object({id: id(), callback, status: 'pending'});
- const cb = function(res, rej) {
- const resolve = (...args) => { this.status = 'resolved'; this.value = args; res(...args); };
- const reject = (...args) => { this.status = 'rejected'; this.value = args; rej(...args); };
- if (this.result) {
- return this.result.then(resolve, reject);
- }
- Object.assign(this, {resolve, reject});
- return callback(resolve, reject);
- }.bind(key);
- super(cb);
- this.resolve = this.resolve.bind(this);
- this.reject = this.reject.bind(this);
- this.key = key;
- }
- resolve(...args) {
- if (this.key.resolve) {
- this.key.resolve(...args);
- } else {
- this.key.result = Promise.resolve(...args);
- }
- return this;
- }
- reject(...args) {
- if (this.key.reject) {
- this.key.reject(...args);
- } else {
- this.key.result = Promise.reject(...args);
- }
- return this;
- }
- addCallback(callback) {
- Promise.resolve().then(() => callback(this.resolve, this.reject));
- return this;
- }
- }
- return PromiseHandler;
- })();
- const {Emitter} = (() => {
- let totalCount = 0;
- let warnings = [];
- class Emitter {
- on(name, callback) {
- if (!this._events) {
- Emitter.totalCount++;
- this._events = new Map();
- }
- name = name.toLowerCase();
- let e = this._events.get(name);
- if (!e) {
- e = this._events.set(name, new Handler(callback));
- } else {
- e.add(callback);
- }
- if (e.length > 10) {
- Emitter.warnings.push(this);
- }
- return this;
- }
- off(name, callback) {
- if (!this._events) {
- return;
- }
- name = name.toLowerCase();
- const e = this._events.get(name);
- if (!this._events.has(name)) {
- return;
- } else if (!callback) {
- this._events.delete(name);
- } else {
- e.remove(callback);
- if (e.isEmpty) {
- this._events.delete(name);
- }
- }
- if (this._events.size < 1) {
- delete this._events;
- }
- return this;
- }
- once(name, func) {
- const wrapper = (...args) => {
- func(...args);
- this.off(name, wrapper);
- wrapper._original = null;
- };
- wrapper._original = func;
- return this.on(name, wrapper);
- }
- clear(name) {
- if (!this._events) {
- return;
- }
- if (name) {
- this._events.delete(name);
- } else {
- delete this._events;
- Emitter.totalCount--;
- }
- return this;
- }
- emit(name, ...args) {
- if (!this._events) {
- return;
- }
- name = name.toLowerCase();
- const e = this._events.get(name);
- if (!e) {
- return;
- }
- e.exec(...args);
- return this;
- }
- emitAsync(...args) {
- if (!this._events) {
- return;
- }
- setTimeout(() => this.emit(...args), 0);
- return this;
- }
- promise(name, callback) {
- if (!this._promise) {
- this._promise = {};
- }
- const p = this._promise[name];
- if (p) {
- return callback ? p.addCallback(callback) : p;
- }
- return this._promise[name] = new PromiseHandler(callback);
- }
- emitResolve(name, ...args) {
- if (!this._promise) {
- this._promise = {};
- }
- if (!this._promise[name]) {
- this._promise[name] = new PromiseHandler();
- }
- this._promise[name].resolve(...args);
- }
- emitReject(name, ...args) {
- if (!this._promise) {
- this._promise = {};
- }
- if (!this._promise[name]) {
- this._promise[name] = new PromiseHandler();
- }
- this._promise[name].reject(...args);
- }
- resetPromise(name) {
- if (!this._promise) { return; }
- delete this._promise[name];
- }
- hasPromise(name) {
- return this._promise && !!this._promise[name];
- }
- }
- Emitter.totalCount = totalCount;
- Emitter.warnings = warnings;
- return {Emitter};
- })();
- return {Handler, PromiseHandler, Emitter};
- }
- const {Handler, PromiseHandler, Emitter} = EmitterInitFunc();
- function parseThumbInfo(xmlText) {
- if (typeof xmlText !== 'string' || xmlText.status === 'ok') {
- return xmlText;
- }
- const parser = new DOMParser();
- const xml = parser.parseFromString(xmlText, 'text/xml');
- const val = name => {
- const elms = xml.getElementsByTagName(name);
- if (elms.length < 1) {
- return null;
- }
- return elms[0].textContent;
- };
- const dateToString = dateString => {
- const date = new Date(dateString);
- const [yy, mm, dd, h, m, s] = [
- date.getFullYear(),
- date.getMonth() + 1,
- date.getDate(),
- date.getHours(),
- date.getMinutes(),
- date.getSeconds()
- ].map(n => n.toString().padStart(2, '0'));
- return `${yy}/${mm}/${dd} ${h}:${m}:${s}`;
- };
- 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 [min, sec] = val('length').split(':');
- const duration = min * 60 + sec * 1;
- const watchId = val('watch_url').split('/').reverse()[0];
- const postedAt = dateToString(new Date(val('first_retrieve')));
- const tags = [...xml.getElementsByTagName('tag')].map(tag => {
- return {
- text: tag.textContent,
- category: tag.hasAttribute('category'),
- lock: tag.hasAttribute('lock')
- };
- });
- const videoId = val('video_id');
- const isChannel = videoId.substring(0, 2) === 'so';
- const result = {
- status: 'ok',
- _format: 'thumbInfo',
- v: isChannel ? videoId : watchId,
- id: videoId,
- videoId,
- watchId: isChannel ? videoId : watchId,
- originalVideoId: (!isChannel && watchId !== videoId) ? videoId : '',
- isChannel,
- title: val('title'),
- description: val('description'),
- thumbnail: val('thumbnail_url').replace(/^http:/, 'https:'),
- movieType: val('movie_type'),
- lastResBody: val('last_res_body'),
- duration,
- 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 && userId !== '') {
- result.owner = {
- type: 'user',
- id: userId,
- linkId: userId ? `user/${userId}` : '',
- name: val('user_nickname') || '(非公開ユーザー)',
- url: userId ? ('https://www.nicovideo.jp/user/' + userId) : '#',
- icon: val('user_icon_url') || 'https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank.jpg'
- };
- }
- const channelId = val('ch_id');
- if (channelId !== null && channelId !== '') {
- result.owner = {
- type: 'channel',
- id: channelId,
- linkId: channelId ? `ch${channelId}` : '',
- name: val('ch_name') || '(非公開チャンネル)',
- url: 'https://ch.nicovideo.jp/ch' + channelId,
- icon: val('ch_icon_url') || 'https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/defaults/blank.jpg'
- };
- }
- return result;
- }
- const workerUtil = (() => {
- let config, TOKEN, PRODUCT = 'ZenzaWatch?', netUtil, CONSTANT, NAME = '';
- let global = null, external = null;
- const isAvailable = !!(window.Blob && window.Worker && window.URL);
- const messageWrapper = function(self) {
- const _onmessage = self.onmessage || (() => {});
- const promises = {};
- const onMessage = async function(self, type, e) {
- const {body, sessionId, status} = e.data;
- const {command, params} = body;
- try {
- let result;
- switch (command) {
- case 'commandResult':
- if (promises[sessionId]) {
- if (status === 'ok') {
- if (params.command === 'fetch') {
- const {buffer, init} = params.result;
- promises[sessionId].resolve(new Response(buffer, init));
- } else {
- promises[sessionId].resolve(params.result);
- }
- } else {
- promises[sessionId].reject(params.result);
- }
- delete promises[sessionId];
- }
- return;
- case 'ping':
- result = {now: Date.now(), NAME, PID, url: location.href};
- break;
- case 'port': {
- const port = e.ports[0];
- portMap[params.name] = port;
- port.addEventListener('message', onMessage.bind({}, port, params.name));
- bindFunc(port, 'MessageChannel');
- if (params.ping) {
- console.time('ping:' + sessionId);
- port.ping().then(result => {
- console.timeEnd('ping:' + sessionId);
- console.log('ok %smec', Date.now() - params.now, params);
- }).catch(err => {
- console.timeEnd('ping:' + sessionId);
- console.warn('ping fail', {err, data: e.data});
- });
- }
- }
- return;
- case 'broadcast': {
- if (!BroadcastChannel) { return; }
- const channel = new BroadcastChannel(`${params.name}`);
- channel.addEventListener('message', onMessage.bind({}, channel, 'BroadcastChannel'));
- bindFunc(channel, 'BroadcastChannel');
- bcast[params.basename] = channel;
- }
- return;
- case 'env':
- ({config, TOKEN, PRODUCT, CONSTANT} = params);
- return;
- default:
- result = await _onmessage({command, params}, type, PID);
- break;
- }
- self.postMessage({body:
- {command: 'commandResult', params:
- {command, result}}, sessionId, TYPE: type, PID, status: 'ok'
- });
- } catch(err) {
- console.error('failed', {err, command, params, sessionId, TYPE: type, PID, data: e.data});
- self.postMessage({body:
- {command: 'commandResult', params: {command, result: err.message || null}},
- sessionId, TYPE: type, PID, status: err.status || 'fail'
- });
- }
- };
- self.onmessage = onMessage.bind({}, self, self.name);
- self.onconnect = e => {
- const port = e.ports[0];
- port.onmessage = self.onmessage;
- port.start();
- };
- const bindFunc = (self, type = 'Worker') => {
- const post = function(self, body, options = {}) {
- const sessionId = `recv:${NAME}:${type}:${this.sessionId++}`;
- return new Promise((resolve, reject) => {
- promises[sessionId] = {resolve, reject};
- self.postMessage({body, sessionId, PID}, options.transfer);
- if (typeof options.timeout === 'number') {
- setTimeout(() => {
- reject({status: 'fail', message: 'timeout'});
- delete promises[sessionId];
- }, options.timeout);
- }
- }).finally(() => { delete promises[sessionId]; });
- };
- const emit = function(self, eventName, data = null) {
- self.post({command: 'emit', params: {eventName, data}});
- };
- const notify = function(self, message) {
- self.post({command: 'notify', params: {message}});
- };
- const alert = function(self, message) {
- self.post({command: 'alert', params: {message}});
- };
- const ping = async function(self, options = {}) {
- const timekey = `PING "${self.name}"`;
- console.log(timekey);
- let result;
- options.timeout = options.timeout || 10000;
- try {
- console.time(timekey);
- result = await self.post({command: 'ping', params: {now: Date.now(), NAME, PID, url: location.href}}, options);
- console.timeEnd(timekey);
- } catch (e) {
- console.timeEnd(timekey);
- console.warn('ping fail', e);
- }
- return result;
- };
- self.post = post.bind({sessionId: 0}, this.port || self);
- self.emit = emit.bind({}, self);
- self.notify = notify.bind({}, self);
- self.alert = alert.bind({}, self);
- self.ping = ping.bind({}, self);
- return self;
- };
- bindFunc(self);
- self.xFetch = (url, options = {}) => {
- options = {...options, ...{signal: null}}; // remove AbortController
- if (url.startsWith(location.origin)) {
- return fetch(url, options);
- }
- return self.post({command: 'fetch', params: {url, options}});
- };
- };
- const workerUtil = {
- isAvailable,
- js: (q, ...args) => {
- const strargs = args.map(a => typeof a === 'string' ? a : a.toString);
- return String.raw(q, ...strargs);
- },
- env: params => {
- ({config, TOKEN, PRODUCT, netUtil, CONSTANT} =
- Object.assign({config, TOKEN, PRODUCT, netUtil, CONSTANT}, params));
- if (global) { ({config, TOKEN, PRODUCT, netUtil, CONSTANT} = global); }
- },
- create: function(func, options = {}) {
- let cache = this.urlMap.get(func);
- const name = options.name || 'Worker';
- if (!cache) {
- const src = `
- const PID = '${window && window.name || 'self'}:${location.href}:${name}:${Date.now().toString(16).toUpperCase()}';
- console.log('%cinit "%s"', 'font-weight: bold;', self.name || '');
- (${func.toString()})(self);
- `;
- const blob = new Blob([src], {type: 'text/javascript'});
- const url = URL.createObjectURL(blob);
- this.urlMap.set(func, url);
- cache = url;
- }
- if (options.type === 'SharedWorker') {
- const w = this.workerMap.get(func) || new SharedWorker(cache);
- this.workerMap.set(func, w);
- return w;
- }
- return new Worker(cache, options);
- }.bind({urlMap: new Map(), workerMap: new Map()}),
- createCrossMessageWorker: function(func, options = {}) {
- const promises = this.promises;
- const name = options.name || 'Worker';
- const PID = `${window && window.name || 'self'}:${location.host}:${name}:${Date.now().toString(16).toUpperCase()}`;
- const _func = `
- function (self) {
- let config = {}, PRODUCT, TOKEN, CONSTANT, NAME = decodeURI('${encodeURI(name)}'), bcast = {}, portMap = {};
- const {Handler, PromiseHandler, Emitter} = (${EmitterInitFunc.toString()})();
- (${func.toString()})(self);
- //===================================
- (${messageWrapper.toString()})(self);
- }
- `;
- const worker = workerUtil.create(_func, options);
- const self = options.type === 'SharedWorker' ? worker.port : worker;
- self.name = name;
- const onMessage = async function(self, e) {
- const {body, sessionId, status} = e.data;
- const {command, params} = body;
- try {
- let result = 'ok';
- let transfer = null;
- switch (command) {
- case 'commandResult':
- if (promises[sessionId]) {
- if (status === 'ok') {
- promises[sessionId].resolve(params.result);
- } else {
- promises[sessionId].reject(params.result);
- }
- delete promises[sessionId];
- }
- return;
- case 'ping':
- result = {now: Date.now(), NAME, PID, url: location.href};
- console.timeLog && console.timeLog(params.NAME, 'PONG');
- break;
- case 'emit':
- global && global.emitter.emitAsync(params.eventName, params.data);
- break;
- case 'fetch':
- result = (netUtil || window).fetch(params.url,
- Object.assign({}, params.options || {}, {_format: 'arraybuffer'}));
- transfer = [result.buffer];
- break;
- case 'notify':
- global && global.notify(params.message);
- break;
- case 'alert':
- global && global.alert(params.message);
- break;
- default:
- self.oncommand && (result = await self.oncommand({command, params}));
- break;
- }
- self.postMessage({body: {command: 'commandResult', params: {command, result}}, sessionId, status: 'ok'}, transfer);
- } catch (err) {
- console.error('failed', {err, command, params, sessionId});
- self.postMessage({body: {command: 'commandResult', params: {command, result: err.message || null}}, sessionId, status: err.status || 'fail'});
- }
- };
- const bindFunc = (self, type = 'Worker') => {
- const post = function(self, body, options = {}) {
- const sessionId = `send:${name}:${type}:${this.sessionId++}`;
- return new Promise((resolve, reject) => {
- promises[sessionId] = {resolve, reject};
- self.postMessage({body, sessionId, TYPE: type, PID}, options.transfer);
- if (typeof options.timeout === 'number') {
- setTimeout(() => {
- reject({status: 'fail', message: 'timeout'});
- delete promises[sessionId];
- }, options.timeout);
- }
- }).finally(() => { delete promises[sessionId]; });
- };
- const ping = async function(self, options = {}) {
- const timekey = `PING "${self.name}" total time`;
- window.console.log(`PING "${self.name}"...`);
- let result;
- options.timeout = options.timeout || 10000;
- try {
- window.console.time(timekey);
- result = await self.post({command: 'ping', params: {now: Date.now(), NAME: self.name, PID, url: location.href}}, options);
- window.console.timeEnd(timekey);
- } catch (e) {
- console.timeEnd(timekey);
- console.warn('ping fail', e);
- }
- return result;
- };
- self.post = post.bind({sessionId: 0}, self);
- self.ping = ping.bind({}, self);
- self.addEventListener('message', onMessage.bind({sessionId: 0}, self));
- self.start && self.start();
- };
- bindFunc(self);
- if (config) {
- self.post({
- command: 'env',
- params: {config: config.export(true), TOKEN, PRODUCT, CONSTANT}
- });
- }
- self.addPort = (port, options = {}) => {
- const name = options.name || 'MessageChannel';
- return self.post({command: 'port', params: {port, name}}, {transfer: [port]});
- };
- const channel = new MessageChannel();
- self.addPort(channel.port2);
- bindFunc(channel.port1, {name: 'MessageChannel'});
- self.bridge = async (worker, options = {}) => {
- const name = options.name || 'MessageChannelBridge';
- const channel = new MessageChannel();
- await self.addPort(channel.port1, {name: worker.name || name});
- await worker.addPort(channel.port2, {name: self.name || name});
- console.log('ping self -> other', await channel.port1.ping());
- console.log('ping other -> self', await channel.port2.ping());
- };
- self.BroadcastChannel = basename => {
- const name = `${basename || 'Broadcast'}${TOKEN || Date.now().toString(16)}`;
- self.post({command: 'broadcast', params: {basename, name}});
- const channel = new BroadcastChannel(name);
- channel.addEventListener('message', onMessage.bind({}, channel, 'BroadcastChannel'));
- bindFunc(channel, 'BroadcastChannel');
- return name;
- };
- self.ping()
- .then(result => window.console.log('OK'))
- .catch(result => console.warn('FAIL', result));
- return self;
- }.bind({
- sessionId: 0,
- promises: {}
- })
- };
- return workerUtil;
- })();
- const IndexedDbStorage = (() => {
- const workerFunc = function(self) {
- const db = {};
- const controller = {
- async init({name, ver, stores}) {
- if (db[name]) {
- return Promise.resolve(db[name]);
- }
- return new Promise((resolve, reject) => {
- const req = indexedDB.open(name, ver);
- req.onupgradeneeded = e => {
- const _db = e.target.result;
- for (const meta of stores) {
- if(_db.objectStoreNames.contains(meta.name)) {
- _db.deleteObjectStore(meta.name);
- }
- const store = _db.createObjectStore(meta.name, meta.definition);
- const indexes = meta.indexes || [];
- for (const idx of indexes) {
- store.createIndex(idx.name, idx.keyPath, idx.params);
- }
- store.transaction.oncomplete = () => {
- console.log('store.transaction.complete', JSON.stringify({name, ver, store: meta}));
- };
- }
- };
- req.onsuccess = e => {
- db[name] = e.target.result;
- resolve(db[name]);
- };
- req.onerror = reject;
- });
- },
- close({name}) {
- if (!db[name]) {
- return;
- }
- db[name].close();
- db[name] = null;
- },
- async getStore({name, storeName, mode = 'readonly'}) {
- const db = await this.init({name});
- return new Promise(async (resolve, reject) => {
- const tx = db.transaction(storeName, mode);
- tx.onerror = reject;
- return resolve({
- store: tx.objectStore(storeName),
- transaction: tx
- });
- });
- },
- async put({name, storeName, data}) {
- const {store, transaction} = await this.getStore({name, storeName, mode: 'readwrite'});
- return new Promise((resolve, reject) => {
- const req = store.put(data);
- req.onsuccess = e => {
- transaction.commit();
- resolve(e.target.result);
- };
- req.onerror = reject;
- });
- },
- async get({name, storeName, data: {key, index, timeout}}) {
- const {store} = await this.getStore({name, storeName});
- return new Promise((resolve, reject) => {
- const req =
- index ?
- store.index(index).get(key) : store.get(key);
- req.onsuccess = e => resolve(e.target.result);
- req.onerror = reject;
- if (timeout) {
- setTimeout(() => {
- reject(`timeout: key${key}`);
- }, timeout);
- }
- });
- },
- async updateTime({name, storeName, data: {key, index, timeout}}) {
- const record = await this.get({name, storeName, data: {key, index, timeout}});
- if (!record) {
- return null;
- }
- record.updatedAt = Date.now();
- this.put({name, storeName, data: record});
- return record;
- },
- async delete({name, storeName, data: {key, index}}) {
- const {store, transaction} = await this.getStore({name, storeName, mode: 'readwrite'});
- return new Promise((resolve, reject) => {
- let remove = 0;
- let range = IDBKeyRange.only(key);
- let req =
- index ?
- store.index(index).openCursor(range) : store.openCursor(range);
- req.onsuccess = e => {
- const result = e.target.result;
- if (!result) {
- transaction.commit();
- return resolve(remove > 0);
- }
- result.delete();
- remove++;
- result.continue();
- };
- req.onerror = reject;
- });
- },
- async clear({name, storeName}) {
- const {store} = await this.getStore({name, storeName, mode: 'readwrite'});
- return new Promise((resolve, reject) => {
- const req = store.clear();
- req.onsuccess = e => {
- console.timeEnd('storage clear');
- resolve();
- };
- req.onerror = e => {
- console.timeEnd('storage clear');
- reject(e);
- };
- });
- },
- async gc({name, storeName, data: {expireTime}}) {
- const {store, transaction} = await this.getStore({name, storeName, mode: 'readwrite'});
- const now = Date.now(), ptime = performance.now();
- const expiresAt = now - expireTime;
- const expireDateTime = new Date(expiresAt).toLocaleString();
- const timekey = `GC [DELETE FROM ${name}.${storeName} WHERE updatedAt < '${expireDateTime}'] `;
- console.time(timekey);
- let count = 0;
- return new Promise((resolve, reject) => {
- const range = IDBKeyRange.upperBound(expiresAt);
- const idx = store.index('updatedAt');
- const req = idx.openCursor(range);
- req.onsuccess = e => {
- const cursor = e.target.result;
- if (cursor) {
- count++;
- cursor.delete();
- return cursor.continue();
- }
- console.timeEnd(timekey);
- resolve({status: 'ok', count, time: performance.now() - ptime});
- };
- req.onerror = reject;
- }).catch((e) => {
- console.error('gc fail', e);
- store.clear();
- });
- }
- };
- const d2a = async dataUrl => fetch(dataUrl).then(r => r.arrayBuffer());
- const a2d = async (arrayBuffer, type = 'image/jpeg') => {
- return new Promise((ok, ng) => {
- const reader = new FileReader();
- reader.onload = () => ok(reader.result);
- reader.onerror = ng;
- reader.readAsDataURL(new Blob([arrayBuffer], {type}));
- });
- };
- self.onmessage = async ({command, params}) => {
- try {
- switch (command) {
- case 'init':
- await controller[command](params);
- return 'ok';
- case 'put': {
- const {name, storeName, data} = params;
- if (data.dataUrls) { // dataURLのままだと肥大化するのでArrayBufferにする
- data.dataUrls = await Promise.all(data.dataUrls.map(url => d2a(url)));
- }
- return controller.put({name, storeName, data});
- }
- case 'updateTime':
- case 'get': {
- const data = await controller[command](params);
- if (data && data.dataUrls) {
- data.dataUrls = await Promise.all(data.dataUrls.map(url => a2d(url)));
- }
- return data;
- }
- default:
- return controller[command](params) || 'ok';
- }
- } catch (err) {
- console.warn('command failed: ', {command, params});
- throw err;
- }
- };
- return controller;
- };
- const workers = {};
- const open = async ({name, ver, stores}, func) => {
- if (!workers[name]) {
- let _func = workerFunc;
- if (func) {
- _func = `
- (() => {
- const controller = (${workerFunc.toString()})(self);
- (${func.toString()})(self)
- })
- `;
- }
- workers[name] = workerUtil.createCrossMessageWorker(_func, {name: `IndexedDb[${name}]`});
- }
- const worker = workers[name];
- worker.post({command: 'init', params: {name, ver, stores}});
- const post = (command, data, storeName, transfer) => {
- const params = {data, name, storeName, transfer};
- return worker.post({command, params}, transfer);
- };
- const result = {worker};
- for (const meta of stores) {
- const storeName = meta.name;
- result[storeName] = (storeName => {
- return {
- close: params => post('close', params, storeName),
- put: (record, transfer) => post('put', record, storeName, transfer),
- get: ({key, index, timeout}) => post('get', {key, index, timeout}, storeName),
- updateTime: ({key, index, timeout}) => post('updateTime', {key, index, timeout}, storeName),
- delete: ({key, index, timeout}) => post('delete', {key, index, timeout}, storeName),
- gc: (expireTime = 30 * 24 * 60 * 60 * 1000) => post('gc', {expireTime}, storeName)
- };
- })(storeName);
- }
- return result;
- };
- return {open};
- })();
- const ThumbInfoCacheDb = (() => {
- const THUMB_INFO = {
- name: 'thumb-info',
- ver: 1,
- stores: [
- {
- name: 'cache',
- indexes: [
- {name: 'postedAt', keyPath: 'postedAt', params: {unique: false}},
- {name: 'updatedAt', keyPath: 'updatedAt', params: {unique: false}}
- ],
- definition: {keyPath: 'watchId', autoIncrement: false}
- }
- ]
- };
- let db;
- const open = async () => {
- db = db || await IndexedDbStorage.open(THUMB_INFO);
- const cacheDb = db['cache'];
- cacheDb.gc(90 * 24 * 60 * 60 * 1000);
- return {
- put: (xml, thumbInfo = null) => {
- thumbInfo = thumbInfo || parseThumbInfo(xml);
- if (thumbInfo.status !== 'ok') {
- return;
- }
- const watchId = thumbInfo.v;
- const videoId = thumbInfo.id;
- const postedAt = new Date(thumbInfo.postedAt).getTime();
- const updatedAt = Date.now();
- const record = {
- watchId,
- videoId,
- postedAt,
- updatedAt,
- xml,
- thumbInfo
- };
- cacheDb.put(record);
- return {watchId, updatedAt};
- },
- get: watchId => cacheDb.updateTime({key: watchId}),
- delete: watchId => cacheDb.delete({key: watchId}),
- close: () => cacheDb.close()
- };
- };
- return {open};
- })();
- window.MylistPocketLib = {
- workerUtil
- };
- const thumbInfoApi = async function() {
- const gate = () => {
- const post = function(body, {type, token, sessionId, origin} = {}) {
- sessionId = sessionId || '';
- origin = origin || '';
- this.origin = origin = origin || this.origin || document.referrer;
- this.token = token = token || this.token;
- this.type = type = type || this.type;
- if (!this.channel) {
- this.channel = new MessageChannel;
- }
- const url = location.href;
- const id = PRODUCT;
- try {
- const msg = {id, type, token, url, sessionId, body};
- if (!this.port) {
- msg.body = {command: 'initialized', params: msg.body};
- parent.postMessage(msg, origin, [this.channel.port2]);
- this.port = this.channel.port1;
- this.port.start();
- } else {
- this.port.postMessage(msg);
- }
- } catch (e) {
- console.error('%cError: parent.postMessage - ', 'color: red; background: yellow', e);
- }
- return this.port;
- }.bind({channel: null, port: null, origin: null, token: null, type: null});
- const parseUrl = url => {
- url = url || 'https://unknown.example.com/';
- const a = document.createElement('a');
- a.href = url;
- return a;
- };
- const isNicoServiceHost = url => {
- const host = parseUrl(url).hostname;
- return /(^[a-z0-9.-]*\.nicovideo\.jp$|^[a-z0-9.-]*\.nico(|:[0-9]+)$)/.test(host);
- };
- const isWhiteHost = url => {
- const u = parseUrl(url);
- const host = u.hostname;
- if (['account.nicovideo.jp', 'point.nicovideo.jp'].includes(host)) {
- return false;
- }
- if (isNicoServiceHost(url)) {
- return true;
- }
- if (['localhost', '127.0.0.1'].includes(host)) { return true; }
- if (localStorage.ZenzaWatch_whiteHost) {
- if (localStorage.ZenzaWatch_whiteHost.split(',').includes(host)) {
- return true;
- }
- }
- if (u.protocol !== 'https:') { return false; }
- return [
- 'google.com',
- 'www.google.com',
- 'www.google.co.jp',
- 'www.bing.com',
- 'twitter.com',
- 'friends.nico',
- 'feedly.com',
- 'www.youtube.com',
- ].includes(host) || host.endsWith('.slack.com');
- };
- const uFetch = params => {
- const {url, options}= params;
- if (!isWhiteHost(url) || !isNicoServiceHost(url)) {
- return Promise.reject({status: 'fail', message: 'network error'});
- }
- const racers = [];
- let timer;
- const timeout = (typeof params.timeout === 'number' && !isNaN(params.timeout)) ? params.timeout : 30 * 1000;
- if (timeout > 0) {
- racers.push(new Promise((resolve, reject) =>
- timer = setTimeout(() => timer ? reject({name: 'timeout', message: 'timeout'}) : resolve(), timeout))
- );
- }
- const controller = AbortController ? (new AbortController()) : null;
- if (controller) {
- params.signal = controller.signal;
- }
- racers.push(fetch(url, options));
- return Promise.race(racers)
- .catch(err => {
- let message = 'uFetch fail';
- if (err && err.name === 'timeout') {
- if (controller) {
- console.warn('request timeout');
- controller.abort();
- }
- message = 'timeout';
- }
- return Promise.reject({status: 'fail', message});
- }).finally(() => { timer && clearTimeout(timer); });
- };
- const xFetch = (params, sessionId = null) => {
- const command = 'fetch';
- return uFetch(params).then(async resp => {
- const buffer = await resp.arrayBuffer();
- const init = ['type', 'url', 'redirected', 'status', 'ok', 'statusText']
- .reduce((map, key) => {map[key] = resp[key]; return map;}, {});
- const headers = [...resp.headers.entries()];
- return Promise.resolve({buffer, init, headers});
- }).then(({buffer, init, headers}) => {
- const result = {status: 'ok', command, params: {buffer, init, headers}};
- post(result, {sessionId});
- return result;
- }).catch(({status, message}) => {
- post({status, message, command}, {sessionId});
- });
- };
- const init = ({prefix, type}) => {
- if (!window.name.startsWith(prefix)) {
- throw new Error(`unknown name "${window.name}"`);
- }
- const PID = `${window && window.name || 'self'}:${location.host}:${name}:${Date.now().toString(16).toUpperCase()}`;
- type = type || window.name.replace(new RegExp(`/(${PRODUCT}|)Loader$/`), '');
- const origin = document.referrer || window.name.split('#')[1];
- console.log('%cCrossDomainGate: %s', 'background: lightgreen;', location.host, window.name, {prefix, type});
- if (!isWhiteHost(origin)) {
- throw new Error(`disable bridge "${origin}"`);
- }
- const TOKEN = location.hash ? location.hash.substring(1) : null;
- window.history.replaceState(null, null, location.pathname);
- const port = post({status: 'ok', command: 'initialized'}, {type, token: TOKEN, origin});
- workerUtil && workerUtil.env({TOKEN, PRODUCT});
- return {port, TOKEN, origin, type, PID};
- };
- return {post, parseUrl, isNicoServiceHost, isWhiteHost, uFetch, xFetch, init};
- };
- const {post, parseUrl, uFetch, init} = gate();
- const {port, TOKEN} = init({prefix: `thumbInfo${PRODUCT}`, type: 'thumbInfo'});
- const db = await ThumbInfoCacheDb.open();
- port.addEventListener('message', async e => {
- const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
- const {body, sessionId, token} = data;
- const {command, params} = body;
- if (command !== 'fetch') { return; }
- const p = parseUrl(params.url);
- if (TOKEN !== token ||
- p.hostname !== location.host ||
- !p.pathname.startsWith('/api/getthumbinfo/')) {
- console.log('invalid msg: ', {origin: e.origin, TOKEN, token, body});
- return;
- }
- params.options = params.options || {};
- const watchId = params.url.split('/').reverse()[0];
- const expiresAt = Date.now() - (params.options.expireTime || 0);
- const cache = await db.get(watchId);
- if (cache && cache.thumbInfo.status === 'ok' && cache.updatedAt < expiresAt) {
- return post({status: 'ok', command, params: cache.thumbInfo}, {sessionId});
- }
- delete params.options.credentials;
- return uFetch(params, sessionId)
- .then(res => res.text())
- .then(async xmlText => {
- let thumbInfo = parseThumbInfo(xmlText);
- if (thumbInfo.status === 'ok') {
- db.put(xmlText, thumbInfo);
- } else if (cache && cache.thumbInfo.status === 'ok') {
- thumbInfo = cache.thumbInfo;
- }
- const result = {status: 'ok', command, params: thumbInfo};
- post(result, {sessionId});
- }).catch(({status, message}) => {
- if (cache && cache.thumbInfo.status === 'ok') {
- return post({status: 'ok', command, params: cache.thumbInfo}, {sessionId});
- }
- return post({status, message, command}, {sessionId});
- });
- });
- };
- const loadGm = () => {
- const script = document.createElement('script');
- script.id = `${PRODUCT}Loader`;
- script.setAttribute('type', 'text/javascript');
- script.setAttribute('charset', 'UTF-8');
- script.append(`
- (() => {
- const {Handler, PromiseHandler, Emitter} = (${EmitterInitFunc.toString()})();
- ${parseThumbInfo.toString()}
- (${monkey.toString()})("${PRODUCT}");
- })();`);
- (document.head || document.documentElement).append(script);
- };
- const host = window.location.host || '';
- if (host === 'ext.nicovideo.jp' &&
- window.name.indexOf(`thumbInfo${PRODUCT}Loader`) >= 0) {
- thumbInfoApi();
- } else if (window === top) {
- loadGm();
- }
- });