Mylist Filter

視聴不可能な動画だけ表示して一括削除とかできるやつ

  1. // ==UserScript==
  2. // @name Mylist Filter
  3. // @namespace https://github.com/segabito/
  4. // @description 視聴不可能な動画だけ表示して一括削除とかできるやつ
  5. // @match *://www.nicovideo.jp/my/mylist*
  6. // @grant none
  7. // @author 名無しさん@匿名希望
  8. // @version 0.0.1
  9. // @run-at document-body
  10. // @license public domain
  11. // @noframes
  12. // ==/UserScript==
  13. /* eslint-disable */
  14.  
  15.  
  16. (async (window) => {
  17. const global = {
  18. PRODUCT: 'MylistFilter'
  19. };
  20. function EmitterInitFunc() {
  21. class Handler { //extends Array {
  22. constructor(...args) {
  23. this._list = args;
  24. }
  25. get length() {
  26. return this._list.length;
  27. }
  28. exec(...args) {
  29. if (!this._list.length) {
  30. return;
  31. } else if (this._list.length === 1) {
  32. this._list[0](...args);
  33. return;
  34. }
  35. for (let i = this._list.length - 1; i >= 0; i--) {
  36. this._list[i](...args);
  37. }
  38. }
  39. execMethod(name, ...args) {
  40. if (!this._list.length) {
  41. return;
  42. } else if (this._list.length === 1) {
  43. this._list[0][name](...args);
  44. return;
  45. }
  46. for (let i = this._list.length - 1; i >= 0; i--) {
  47. this._list[i][name](...args);
  48. }
  49. }
  50. add(member) {
  51. if (this._list.includes(member)) {
  52. return this;
  53. }
  54. this._list.unshift(member);
  55. return this;
  56. }
  57. remove(member) {
  58. this._list = this._list.filter(m => m !== member);
  59. return this;
  60. }
  61. clear() {
  62. this._list.length = 0;
  63. return this;
  64. }
  65. get isEmpty() {
  66. return this._list.length < 1;
  67. }
  68. *[Symbol.iterator]() {
  69. const list = this._list || [];
  70. for (const member of list) {
  71. yield member;
  72. }
  73. }
  74. next() {
  75. return this[Symbol.iterator]();
  76. }
  77. }
  78. Handler.nop = () => {/* ( ˘ω˘ ) スヤァ */};
  79. const PromiseHandler = (() => {
  80. const id = function() { return `Promise${this.id++}`; }.bind({id: 0});
  81. class PromiseHandler extends Promise {
  82. constructor(callback = () => {}) {
  83. const key = new Object({id: id(), callback, status: 'pending'});
  84. const cb = function(res, rej) {
  85. const resolve = (...args) => { this.status = 'resolved'; this.value = args; res(...args); };
  86. const reject = (...args) => { this.status = 'rejected'; this.value = args; rej(...args); };
  87. if (this.result) {
  88. return this.result.then(resolve, reject);
  89. }
  90. Object.assign(this, {resolve, reject});
  91. return callback(resolve, reject);
  92. }.bind(key);
  93. super(cb);
  94. this.resolve = this.resolve.bind(this);
  95. this.reject = this.reject.bind(this);
  96. this.key = key;
  97. }
  98. resolve(...args) {
  99. if (this.key.resolve) {
  100. this.key.resolve(...args);
  101. } else {
  102. this.key.result = Promise.resolve(...args);
  103. }
  104. return this;
  105. }
  106. reject(...args) {
  107. if (this.key.reject) {
  108. this.key.reject(...args);
  109. } else {
  110. this.key.result = Promise.reject(...args);
  111. }
  112. return this;
  113. }
  114. addCallback(callback) {
  115. Promise.resolve().then(() => callback(this.resolve, this.reject));
  116. return this;
  117. }
  118. }
  119. return PromiseHandler;
  120. })();
  121. const {Emitter} = (() => {
  122. let totalCount = 0;
  123. let warnings = [];
  124. class Emitter {
  125. on(name, callback) {
  126. if (!this._events) {
  127. Emitter.totalCount++;
  128. this._events = new Map();
  129. }
  130. name = name.toLowerCase();
  131. let e = this._events.get(name);
  132. if (!e) {
  133. e = this._events.set(name, new Handler(callback));
  134. } else {
  135. e.add(callback);
  136. }
  137. if (e.length > 10) {
  138. !Emitter.warnings.includes(this) && Emitter.warnings.push(this);
  139. }
  140. return this;
  141. }
  142. off(name, callback) {
  143. if (!this._events) {
  144. return;
  145. }
  146. name = name.toLowerCase();
  147. const e = this._events.get(name);
  148. if (!this._events.has(name)) {
  149. return;
  150. } else if (!callback) {
  151. this._events.delete(name);
  152. } else {
  153. e.remove(callback);
  154. if (e.isEmpty) {
  155. this._events.delete(name);
  156. }
  157. }
  158. if (this._events.size < 1) {
  159. delete this._events;
  160. }
  161. return this;
  162. }
  163. once(name, func) {
  164. const wrapper = (...args) => {
  165. func(...args);
  166. this.off(name, wrapper);
  167. wrapper._original = null;
  168. };
  169. wrapper._original = func;
  170. return this.on(name, wrapper);
  171. }
  172. clear(name) {
  173. if (!this._events) {
  174. return;
  175. }
  176. if (name) {
  177. this._events.delete(name);
  178. } else {
  179. delete this._events;
  180. Emitter.totalCount--;
  181. }
  182. return this;
  183. }
  184. emit(name, ...args) {
  185. if (!this._events) {
  186. return;
  187. }
  188. name = name.toLowerCase();
  189. const e = this._events.get(name);
  190. if (!e) {
  191. return;
  192. }
  193. e.exec(...args);
  194. return this;
  195. }
  196. emitAsync(...args) {
  197. if (!this._events) {
  198. return;
  199. }
  200. setTimeout(() => this.emit(...args), 0);
  201. return this;
  202. }
  203. promise(name, callback) {
  204. if (!this._promise) {
  205. this._promise = new Map;
  206. }
  207. const p = this._promise.get(name);
  208. if (p) {
  209. return callback ? p.addCallback(callback) : p;
  210. }
  211. this._promise.set(name, new PromiseHandler(callback));
  212. return this._promise.get(name);
  213. }
  214. emitResolve(name, ...args) {
  215. if (!this._promise) {
  216. this._promise = new Map;
  217. }
  218. if (!this._promise.has(name)) {
  219. this._promise.set(name, new PromiseHandler());
  220. }
  221. return this._promise.get(name).resolve(...args);
  222. }
  223. emitReject(name, ...args) {
  224. if (!this._promise) {
  225. this._promise = new Map;
  226. }
  227. if (!this._promise.has(name)) {
  228. this._promise.set(name, new PromiseHandler);
  229. }
  230. return this._promise.get(name).reject(...args);
  231. }
  232. resetPromise(name) {
  233. if (!this._promise) { return; }
  234. this._promise.delete(name);
  235. }
  236. hasPromise(name) {
  237. return this._promise && this._promise.has(name);
  238. }
  239. addEventListener(...args) { return this.on(...args); }
  240. removeEventListener(...args) { return this.off(...args);}
  241. }
  242. Emitter.totalCount = totalCount;
  243. Emitter.warnings = warnings;
  244. return {Emitter};
  245. })();
  246. return {Handler, PromiseHandler, Emitter};
  247. }
  248. const {Handler, PromiseHandler, Emitter} = EmitterInitFunc();
  249. const dimport = (() => {
  250. try { // google先生の真似
  251. return new Function('u', 'return import(u)');
  252. } catch(e) {
  253. const map = {};
  254. let count = 0;
  255. return url => {
  256. if (map[url]) {
  257. return map[url];
  258. }
  259. try {
  260. const now = Date.now();
  261. const callbackName = `dimport_${now}_${count++}`;
  262. const loader = `
  263. import * as module${now} from "${url}";
  264. console.log('%cdynamic import from "${url}"',
  265. 'font-weight: bold; background: #333; color: #ff9; display: block; padding: 4px; width: 100%;');
  266. window.${callbackName}(module${now});
  267. `.trim();
  268. window.console.time(`"${url}" import time`);
  269. const p = new Promise((ok, ng) => {
  270. const s = document.createElement('script');
  271. s.type = 'module';
  272. s.onerror = ng;
  273. s.append(loader);
  274. s.dataset.import = url;
  275. window[callbackName] = module => {
  276. window.console.timeEnd(`"${url}" import time`);
  277. ok(module);
  278. delete window[callbackName];
  279. };
  280. document.head.append(s);
  281. });
  282. map[url] = p;
  283. return p;
  284. } catch (e) {
  285. console.warn(url, e);
  286. return Promise.reject(e);
  287. }
  288. };
  289. }
  290. })();
  291. const bounce = {
  292. origin: Symbol('origin'),
  293. raf(func) {
  294. let reqId = null;
  295. let lastArgs = null;
  296. const callback = () => {
  297. const lastResult = func(...lastArgs);
  298. reqId = lastArgs = null;
  299. };
  300. const result = (...args) => {
  301. if (reqId) {
  302. cancelAnimationFrame(reqId);
  303. }
  304. lastArgs = args;
  305. reqId = requestAnimationFrame(callback);
  306. };
  307. result[this.origin] = func;
  308. return result;
  309. },
  310. idle(func, time) {
  311. let reqId = null;
  312. let lastArgs = null;
  313. let promise = new PromiseHandler();
  314. const [caller, canceller] =
  315. (time === undefined && window.requestIdleCallback) ?
  316. [window.requestIdleCallback, window.cancelIdleCallback] : [window.setTimeout, window.clearTimeout];
  317. const callback = () => {
  318. const lastResult = func(...lastArgs);
  319. promise.resolve({lastResult, lastArgs});
  320. reqId = lastArgs = null;
  321. promise = new PromiseHandler();
  322. };
  323. const result = (...args) => {
  324. if (reqId) {
  325. reqId = canceller(reqId);
  326. }
  327. lastArgs = args;
  328. reqId = caller(callback, time);
  329. return promise;
  330. };
  331. result[this.origin] = func;
  332. return result;
  333. },
  334. time(func, time = 0) {
  335. return this.idle(func, time);
  336. }
  337. };
  338. const throttle = (func, interval) => {
  339. let lastTime = 0;
  340. let timer;
  341. let promise = new PromiseHandler();
  342. const result = (...args) => {
  343. const now = performance.now();
  344. const timeDiff = now - lastTime;
  345. if (timeDiff < interval) {
  346. if (!timer) {
  347. timer = setTimeout(() => {
  348. lastTime = performance.now();
  349. timer = null;
  350. const lastResult = func(...args);
  351. promise.resolve({lastResult, lastArgs: args});
  352. promise = new PromiseHandler();
  353. }, Math.max(interval - timeDiff, 0));
  354. }
  355. return;
  356. }
  357. if (timer) {
  358. timer = clearTimeout(timer);
  359. }
  360. lastTime = now;
  361. const lastResult = func(...args);
  362. promise.resolve({lastResult, lastArgs: args});
  363. promise = new PromiseHandler();
  364. };
  365. result.cancel = () => {
  366. if (timer) {
  367. timer = clearTimeout(timer);
  368. }
  369. promise.resolve({lastResult: null, lastArgs: null});
  370. promise = new PromiseHandler();
  371. };
  372. return result;
  373. };
  374. throttle.raf = func => {
  375. let raf;
  376. const result = (...args) => {
  377. if (raf) {
  378. return;
  379. }
  380. raf = requestAnimationFrame(() => {
  381. raf = null;
  382. func(...args);
  383. });
  384. };
  385. result.cancel = () => {
  386. if (raf) {
  387. raf = cancelAnimationFrame(raf);
  388. }
  389. };
  390. return result;
  391. };
  392. throttle.idle = func => {
  393. let id;
  394. const request = (self.requestIdleCallback || self.setTimeout);
  395. const cancel = (self.cancelIdleCallback || self.clearTimeout);
  396. const result = (...args) => {
  397. if (id) {
  398. return;
  399. }
  400. id = request(() => {
  401. id = null;
  402. func(...args);
  403. }, 0);
  404. };
  405. result.cancel = () => {
  406. if (id) {
  407. id = cancel(id);
  408. }
  409. };
  410. return result;
  411. };
  412. const css = (() => {
  413. const setPropsTask = [];
  414. const applySetProps = throttle.raf(() => {
  415. const tasks = setPropsTask.concat();
  416. setPropsTask.length = 0;
  417. for (const [element, prop, value] of tasks) {
  418. try {
  419. element.style.setProperty(prop, value);
  420. } catch (error) {
  421. console.warn('element.style.setProperty fail', {element, prop, value, error});
  422. }
  423. }
  424. });
  425. const css = {
  426. addStyle: (styles, option, document = window.document) => {
  427. const elm = Object.assign(document.createElement('style'), {
  428. type: 'text/css'
  429. }, typeof option === 'string' ? {id: option} : (option || {}));
  430. if (typeof option === 'string') {
  431. elm.id = option;
  432. } else if (option) {
  433. Object.assign(elm, option);
  434. }
  435. elm.classList.add(global.PRODUCT);
  436. elm.append(styles.toString());
  437. (document.head || document.body || document.documentElement).append(elm);
  438. elm.disabled = option && option.disabled;
  439. elm.dataset.switch = elm.disabled ? 'off' : 'on';
  440. return elm;
  441. },
  442. registerProps(...args) {
  443. if (!CSS || !('registerProperty' in CSS)) {
  444. return;
  445. }
  446. for (const definition of args) {
  447. try {
  448. (definition.window || window).CSS.registerProperty(definition);
  449. } catch (err) { console.warn('CSS.registerProperty fail', definition, err); }
  450. }
  451. },
  452. setProps(...tasks) {
  453. setPropsTask.push(...tasks);
  454. if (setPropsTask.length) {
  455. applySetProps();
  456. }
  457. return Promise.resolve();
  458. },
  459. addModule: async function(func, options = {}) {
  460. if (!CSS || !('paintWorklet' in CSS) || this.set.has(func)) {
  461. return;
  462. }
  463. this.set.add(func);
  464. const src =
  465. `(${func.toString()})(
  466. this,
  467. registerPaint,
  468. ${JSON.stringify(options.config || {}, null, 2)}
  469. );`;
  470. const blob = new Blob([src], {type: 'text/javascript'});
  471. const url = URL.createObjectURL(blob);
  472. await CSS.paintWorklet.addModule(url).then(() => URL.revokeObjectURL(url));
  473. return true;
  474. }.bind({set: new WeakSet}),
  475. escape: value => CSS.escape ? CSS.escape(value) : value.replace(/([\.#()[\]])/g, '\\$1'),
  476. number: value => CSS.number ? CSS.number(value) : value,
  477. s: value => CSS.s ? CSS.s(value) : `${value}s`,
  478. ms: value => CSS.ms ? CSS.ms(value) : `${value}ms`,
  479. pt: value => CSS.pt ? CSS.pt(value) : `${value}pt`,
  480. px: value => CSS.px ? CSS.px(value) : `${value}px`,
  481. percent: value => CSS.percent ? CSS.percent(value) : `${value}%`,
  482. vh: value => CSS.vh ? CSS.vh(value) : `${value}vh`,
  483. vw: value => CSS.vw ? CSS.vw(value) : `${value}vw`,
  484. trans: value => self.CSSStyleValue ? CSSStyleValue.parse('transform', value) : value,
  485. word: value => self.CSSKeywordValue ? new CSSKeywordValue(value) : value,
  486. image: value => self.CSSStyleValue ? CSSStyleValue.parse('background-image', value) : value,
  487. };
  488. return css;
  489. })();
  490. const cssUtil = css;
  491. const [lit] = await Promise.all([
  492. dimport('https://unpkg.com/lit-html?module')
  493. ]);
  494. const {html} = lit;
  495. const $ = self.jQuery;
  496.  
  497. cssUtil.addStyle(`
  498. .ItemSelectMenuContainer-itemSelect {
  499. display: grid;
  500. grid-template-columns: 160px 1fr
  501. }
  502.  
  503. .itemFilterContainer {
  504. display: grid;
  505. background: #f0f0f0;
  506. grid-template-rows: 1fr 1fr;
  507. grid-template-columns: auto 1fr;
  508. user-select: none;
  509. }
  510.  
  511. .itemFilterContainer-title {
  512. grid-row: 1 / 3;
  513. grid-column: 1 / 2;
  514. display: flex;
  515. align-items: center;
  516. white-space: nowrap;
  517. padding: 8px;
  518. }
  519.  
  520. .playableFilter {
  521. grid-row: 1;
  522. grid-column: 2;
  523. padding: 4px 8px;
  524. }
  525.  
  526. .wordFilter {
  527. grid-row: 2;
  528. grid-column: 2;
  529. padding: 0 8px 4px;
  530. }
  531.  
  532. .playableFilter, .wordFilter {
  533. display: inline-flex;
  534. align-items: center;
  535. }
  536.  
  537. .playableFilter .caption, .wordFilter .caption {
  538. display: inline-block;
  539. margin-right: 8px;
  540. }
  541.  
  542. .playableFilter input[type="radio"] {
  543. transform: scale(1.2);
  544. margin-right: 8px;
  545. }
  546.  
  547. .playableFilter label {
  548. display: inline-flex;
  549. align-items: center;
  550. padding: 0 8px;
  551. }
  552.  
  553. .playableFilter input[checked] + span {
  554. background: linear-gradient(transparent 80%, #99ccff 0%);
  555. }
  556.  
  557. .wordFilter input[type="text"] {
  558. padding: 4px;
  559. }
  560. .wordFilter input[type="button"] {
  561. padding: 4px;
  562. border: 1px solid #ccc;
  563. }
  564. .wordFilter input[type="button"]:hover::before {
  565. content: '・';
  566. }
  567. .wordFilter input[type="button"]:hover::after {
  568. content: '・';
  569. }
  570. `);
  571.  
  572. const playableFilterTpl = props => {
  573. const playable = props.playable || '';
  574. return html`
  575. <div class="playableFilter">
  576. <span class="caption">状態</span>
  577. <label
  578. data-click-command="set-playable-filter"
  579. data-command-param=""
  580. >
  581. <input type="radio" name="playable-filter" value=""
  582. ?checked=${playable !== 'playable' && playable !== 'not-playable'}>
  583. <span>指定なし</span>
  584. </label>
  585. <label
  586. data-click-command="set-playable-filter"
  587. data-command-param="playable"
  588. >
  589. <input type="radio" name="playable-filter" value="playable"
  590. ?checked=${playable === 'playable'}>
  591. <span>視聴可能</span>
  592. </label>
  593. <label
  594. data-click-command="set-playable-filter"
  595. data-command-param="not-playable"
  596. >
  597. <input type="radio" name="playable-filter" value="not-playable"
  598. ?checked=${playable === 'not-playable'}>
  599. <span>視聴不可</span>
  600. </label>
  601. </div>`;
  602. };
  603.  
  604. const wordFilterTpl = props => {
  605. return html`
  606. <div class="wordFilter">
  607. <input type="text" name="word-filter" class="wordFilterInput" placeholder="キーワード"
  608. value=${props.word || ''}>
  609. <input type="button" data-click-command="clear-word-filter"
  610. title="・✗・" value=" ✗ ">
  611. <small> タイトル・マイリストコメント検索</small>
  612. </div>`;
  613. };
  614.  
  615. const resetForm = () => {
  616. [...document.querySelectorAll('.itemFilterContainer input[name="playable-filter"]')]
  617. .forEach(r => r.checked = r.hasAttribute('checked'));
  618. [...document.querySelectorAll('.wordFilterInput')]
  619. .forEach(r => r.value = r.getAttribute('value'));
  620. };
  621.  
  622. const itemFilterContainer = Object.assign(document.createElement('div'), {
  623. className: 'itemFilterContainer'
  624. });
  625.  
  626. const render = props => {
  627. if (!document.body.contains(itemFilterContainer)) {
  628. const parentNode = document.querySelector('.ItemSelectMenuContainer-itemSelect');
  629. if (parentNode) {
  630. parentNode.append(itemFilterContainer);
  631. }
  632. }
  633.  
  634. lit.render(html`
  635. <div class="itemFilterContainer-title">絞り込み</div>
  636. ${playableFilterTpl(props)}
  637. ${wordFilterTpl(props)}
  638. `, itemFilterContainer);
  639.  
  640. resetForm();
  641. };
  642.  
  643. let override = false;
  644. const overrideFilter = () => {
  645. if (!window.MylistHelper || override) {
  646. return;
  647. }
  648. override = true;
  649. const self = window.MylistHelper.itemFilter;
  650. Object.defineProperty(self, 'wordFilterCallback', {
  651. get: () => {
  652. const word = self.word.trim();
  653.  
  654. return word ?
  655. item => {
  656. return (
  657. (item.item_data.title || '') .toLowerCase().indexOf(word) >= 0 ||
  658. (item.item_data.description || '') .toLowerCase().indexOf(word) >= 0 ||
  659. (item.description || '') .toLowerCase().indexOf(word) >= 0
  660. );
  661. } :
  662. () => true
  663. ;
  664. }
  665. });
  666. };
  667.  
  668. const parseProps = () => {
  669. if (!location.hash || location.length <= 2) { return {}; }
  670. return location.hash.substring(1).split('+').reduce((map, entry) => {
  671. const [key, val] = entry.split('=').map(e => decodeURIComponent(e));
  672. map[key] = val;
  673. return map;
  674. }, {});
  675. };
  676.  
  677. const update = () => {
  678. overrideFilter();
  679. const props = parseProps();
  680. // console.log('update form', props);
  681. render(props);
  682. };
  683.  
  684. const init = () => {
  685. const _update = bounce.time(update, 100);
  686. _update();
  687. $('.content').on('nicoPageChanged', _update);
  688. };
  689.  
  690. $(() => init());
  691. })(globalThis ? globalThis.window : window);
  692.