Niconico Tag Allegation Autofill

ローカル保存または既定値でタグ通報フォームを自動入力。

目前為 2025-09-08 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Niconico Tag Allegation Autofill
// @namespace    https://gf.qytechs.cn/users/prozent55
// @version      1.0.1
// @description  ローカル保存または既定値でタグ通報フォームを自動入力。
// @match        https://www.nicovideo.jp/comment_allegation/*
// @run-at       document-idle
// @connect      ext.nicovideo.jp
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlHttpRequest
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  // ===== defaults =====
  const DEFAULT_TARGET = 'tag';
  const DEFAULT_ITEM   = 'search_interference';
  const DEFAULT_REASON_BODY =
    '動画の内容とは無関係なタグがロックされており、削除できません。\n' +
    'タグ検索や関連機能に支障をきたすため、荒らし行為にあたり利用規約違反の可能性があると判断しました。';

  // ===== WL (localStorage) =====
  const WL_KEY = 'zr_tag_wl_v1';
  const WL_DEFAULT = [
    '真夏の夜の淫夢','淫夢実況シリーズ','ひとくち淫夢','淫夢本編リンク','本編改造淫夢','BB先輩シリーズ',"ホラー淫夢","タクヤさん"
  ];
  const loadWL = () => { try { const s = localStorage.getItem(WL_KEY); return s ? JSON.parse(s) : WL_DEFAULT.slice(); } catch { return WL_DEFAULT.slice(); } };
  const saveWL = (arr) => { try { localStorage.setItem(WL_KEY, JSON.stringify(arr||[])); } catch {} };

  // ===== form cache (read only) =====
  const KP = 'zippy_nico_tag_form_';
  const K_TARGET = KP + 'target';
  const K_ITEM   = KP + 'item';
  const K_TEXT   = KP + 'text';
  const storage = {
    async get(k, d){ try{ const v = localStorage.getItem('__'+k); return v==null?d:JSON.parse(v);}catch{ return d; } },
    async set(k, v){ try{ localStorage.setItem('__'+k, JSON.stringify(v)); }catch{} }
  };

  // ===== util =====
  const norm = s => (s||'').trim().toLowerCase();
  const qs  = (s, r=document) => r.querySelector(s);
  const qsa = (s, r=document) => Array.from(r.querySelectorAll(s));
  const fire = (el) => { if (!el) return; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); };
  function setVal(el, val){
    const proto = el?.constructor?.prototype || HTMLTextAreaElement.prototype;
    const d = proto && Object.getOwnPropertyDescriptor(proto,'value');
    if (d?.set) d.set.call(el, val); else el.value = val;
    fire(el);
  }
  function toast(msg,ms=1200){ const n=document.createElement('div'); n.textContent=msg; Object.assign(n.style,{position:'fixed',right:'16px',bottom:'110px',zIndex:999999,background:'#00c853',color:'#fff',padding:'8px 10px',borderRadius:'8px',boxShadow:'0 4px 12px rgba(0,0,0,.25)',opacity:'0',transition:'opacity .15s'}); document.body.appendChild(n); requestAnimationFrame(()=>n.style.opacity='1'); setTimeout(()=>{ n.style.opacity='0'; setTimeout(()=>n.remove(),180); },ms); }

  function waitForForm(ms=8000){
    return new Promise(res=>{
      const pick = () => {
        const radios = qsa('input[type="radio"][name="target"]');
        const select = qs('select[name="select_allegation"]');
        const ta     = qs('textarea[name="inquiry"]#inquiry');
        return (radios.length && select && ta) ? {radios, select, ta} : null;
      };
      const first = pick(); if (first) return res(first);
      const to = setTimeout(()=>{ mo.disconnect(); res(null); }, ms);
      const mo = new MutationObserver(()=>{ const f = pick(); if (f){ clearTimeout(to); mo.disconnect(); res(f); }});
      mo.observe(document.body,{childList:true,subtree:true});
    });
  }

  // ===== getthumbinfo =====
  function videoIdFromPath(){ const m=location.pathname.match(/\/comment_allegation\/([a-z]{2}\d+)/i); return m?m[1]:null; }
  function httpGet(url){
    return new Promise((resolve,reject)=>{
      const fn = (typeof GM?.xmlHttpRequest==='function')?GM.xmlHttpRequest:(typeof GM_xmlhttpRequest==='function')?GM_xmlHttpRequest:null;
      if (!fn) return reject(new Error('GM.xmlHttpRequest not available'));
      fn({ method:'GET', url, onload:r=>resolve(r.responseText), onerror:reject });
    });
  }
  async function fetchTags(videoId){
    if (!videoId) return [];
    const url = `https://ext.nicovideo.jp/api/getthumbinfo/${encodeURIComponent(videoId)}`;
    let xml=''; try{ xml=await httpGet(url); }catch{ return []; }
    let doc; try{ doc=new DOMParser().parseFromString(xml,'text/xml'); }catch{ return []; }
    const nodes = Array.from(doc.querySelectorAll('thumb > tags > tag, tags > tag'));
    return nodes.map(t=>({ name:(t.textContent||'').trim(), locked:(t.getAttribute('lock')==='1'||t.getAttribute('locked')==='1') }))
                .filter(x=>x.name);
  }
  function filterWLAllLocked(thumbTags, wl){
    const src = thumbTags.filter(t=>t.locked).map(t=>t.name);
    if (!src.length || !wl.length) return [];
    const S = src.map(norm); const out=[];
    for (const w of wl){ const i=S.indexOf(norm(w)); if(i!==-1 && !out.includes(src[i])) out.push(src[i]); }
    return out;
  }

  // ===== body compose =====
  const TAG_LINE_RE = /^【タグの内容】.*(?:\r?\n)?/m;
  function composeWithTagLine(currentText, tags){
    const body0 = (currentText || '').replace(TAG_LINE_RE, '');
    const tagLineOnly = `【タグの内容】\n${tags.length ? tags.join('、') : '(未特定)'}\n`;
    const body = body0.trim()
      ? body0.replace(/^\r?\n+/, '')
      : `【違反と判断された理由】\n${DEFAULT_REASON_BODY}`;
    return tagLineOnly + body;
  }

  // ===== panel (WL edit / reset) =====
  function panel(nodes, reapply){
    if (document.getElementById('zr-min2-host')) return;
    const host = document.createElement('div');
    host.id = 'zr-min2-host';
    Object.assign(host.style,{position:'fixed',right:'16px',bottom:'16px',zIndex:999999});
    document.body.appendChild(host);
    const root = host.attachShadow({ mode:'open' });
    const wrap = document.createElement('div');
    wrap.className = 'zr-min2';
    wrap.innerHTML = `
      <div class="row"><b>自動入力(ローカル⇄既定)</b></div>
      <div class="row">
        <button id="zr-edit" type="button">編集</button>
        <button id="zr-reset" type="button">既定に戻す</button>
      </div>
    `;
    const style = document.createElement('style');
    style.textContent = `
      :host { all: initial; }
      .zr-min2 { all: initial; display:block; font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans JP","Hiragino Kaku Gothic ProN",Meiryo,sans-serif; color:#fff; background:#0b1220cc; backdrop-filter:blur(6px); padding:10px 12px; border-radius:10px; box-shadow:0 8px 20px rgba(0,0,0,.35); }
      .row { all: initial; display:block; margin:6px 0; font:inherit; color:inherit; }
      b { all: initial; font:inherit; font-weight:700; color:inherit; }
      button { all: initial; font:inherit; color:#fff; background:#1f6feb; padding:6px 10px; border-radius:6px; cursor:pointer; margin-right:6px; box-shadow:0 1px 2px rgba(0,0,0,.25); }
      button:hover { background:#2b7af3; }
      button:active { background:#195bd0; }
      button#zr-reset { background:#d93025; }
    `;
    root.append(style, wrap);
    root.getElementById('zr-edit').addEventListener('click', () => {
      const cur = JSON.stringify(loadWL(), null, 2);
      const nxt = prompt('ホワイトリスト(JSON配列):', cur);
      if (!nxt) return;
      try { saveWL(JSON.parse(nxt)); reapply({ forceDefaultBody:false }); toast('編集を保存しました'); }
      catch { alert('JSONが不正です'); }
    });
    root.getElementById('zr-reset').addEventListener('click', () => {
      saveWL(WL_DEFAULT.slice());
      reapply({ forceDefaultBody:true });
      toast('既定に戻しました');
    });
  }

  // ===== main =====
  (async function main(){
    const nodes = await waitForForm();
    if (!nodes) return;
    const { radios, select, ta } = nodes;

    const savedTarget = await storage.get(K_TARGET, '');
    const savedItem   = await storage.get(K_ITEM,   '');
    const savedText   = await storage.get(K_TEXT,   '');

    const target = savedTarget || DEFAULT_TARGET;
    const item   = savedItem   || DEFAULT_ITEM;

    const r = radios.find(x => x.value === String(target)); if (r){ r.checked = true; fire(r); }
    if ([...select.options].some(o => o.value === item)) { select.value = item; fire(select); }

    const videoId   = videoIdFromPath();
    const thumbTags = await fetchTags(videoId);

    const applyNow = ({ forceDefaultBody = false } = {}) => {
      const wl  = loadWL();
      const hit = filterWLAllLocked(thumbTags, wl);
      const cur = forceDefaultBody ? '' : (savedText || ta.value || '');
      const next = composeWithTagLine(cur, hit);
      setVal(ta, next);
      const check = ta.value || '';
      const ensured = check.replace(/^【タグの内容】[^\r\n]*\r?\n?/, (m) => m.endsWith('\n') ? m : (m + '\n'));
      if (ensured !== check) setVal(ta, ensured);
    };

    applyNow();
    panel(nodes, applyNow);
  })();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址