Narou API Info + Author Page Button (in Box)

なろうの小説トップページ上にAPIから取得した作品情報を表示、キーワード強調、作者マイページリンクボタンも情報ボックス内に表示

当前为 2025-06-05 提交的版本,查看 最新版本

// ==UserScript==
// @name         Narou API Info + Author Page Button (in Box)
// @namespace    haaarug
// @version      2.5
// @description  なろうの小説トップページ上にAPIから取得した作品情報を表示、キーワード強調、作者マイページリンクボタンも情報ボックス内に表示
// @license      CC0
// @match        https://ncode.syosetu.com/*
// @grant        GM_xmlhttpRequest
// @connect      api.syosetu.com
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';
    const NGwords = ["残酷", "NG2", "NG3", "NG4", "NG5"];
    const OKwords = ["異世界", "OK2", "OK3", "OK4", "OK5"];

    // 話数ページではなく作品トップページかを確認
    const pathSegments = location.pathname.split('/').filter(Boolean);
    if (pathSegments.length !== 1) return;

    const match = pathSegments[0].match(/^(n\d+[a-z]+)$/i);
    if (!match) return;

    const ncode = match[1].toLowerCase();
    const apiUrl = `https://api.syosetu.com/novelapi/api/?out=json&ncode=${encodeURIComponent(ncode)}`;

    // 作者リンクの要素を探す
    const authorLinkEl = document.querySelector('a.c-under-nav__item[href^="https://mypage.syosetu.com/"]');
    const authorPageUrl = authorLinkEl ? authorLinkEl.href : null;

    // APIリクエスト
    GM_xmlhttpRequest({
        method: 'GET',
        url: apiUrl,
        headers: { 'Accept': 'application/json' },
        onload: function (response) {
            try {
                const json = JSON.parse(response.responseText);
                if (json.length < 2) return;

                const data = json[1];
                const title = data.title || '不明';
                const writer = data.writer || '不明';
                const status = data.end === 0 ? '完結' : '連載中❌';
                const eternal = data.isstop === 0 ? '' : '⚠️エタ?⚠️';
                const keywords = data.keyword || '不明';
                // NGワード, OKワードを含むキーワードを目立たせる
                const highlightedKeywords = keywords.split(" ").map(word => {
                    if (NGwords.some(ng => word.includes(ng))) {
                        // NGワードを含む
                        return `<span style="color: red; font-weight: bold; font-size: 22px;">${word}</span>`;
                    } else if (OKwords.some(ok => word.includes(ok))) {
                        // OKワードと完全一致
                        return `<span style="color: green;">${word}</span>`;
                    } else {
                        // どちらにも該当しない
                        return `${word}`;
                    }
                }).join(" ");
                const length = data.length ? data.length.toLocaleString() + '文字' : '不明';
                const general_lastup = data.general_lastup || '不明';
                const general_all_no = data.general_all_no ? data.general_all_no.toLocaleString() + '話' : '不明';
                const genreMap = {
                    0: '未選択〔未選択〕', 101: '異世界〔恋愛〕', 102: '現実世界〔恋愛〕',
                    201: 'ハイファンタジー〔ファンタジー〕', 202: 'ローファンタジー〔ファンタジー〕',
                    301: '純文学〔文芸〕', 302: 'ヒューマンドラマ〔文芸〕', 303: '歴史〔文芸〕',
                    304: '推理〔文芸〕', 305: 'ホラー〔文芸〕', 306: 'アクション〔文芸〕',
                    307: 'コメディー〔文芸〕', 401: 'VRゲーム〔SF〕', 402: '宇宙〔SF〕',
                    403: '空想科学〔SF〕', 404: 'パニック〔SF〕',
                    9901: '童話〔その他〕', 9902: '詩〔その他〕', 9903: 'エッセイ〔その他〕',
                    9904: 'リプレイ〔その他〕', 9999: 'その他〔その他〕', 9801: 'ノンジャンル〔ノンジャンル〕'
                };
                const genreText = genreMap[data.genre] || '不明ジャンル';

                // 開閉ボタン
                const toggleButton = document.createElement('button');
                toggleButton.textContent = 'ℹ️';
                toggleButton.style.cssText = `
                    position: fixed;
                    top: 10px;
                    left: 10px;
                    z-index: 10000;
                    padding: 5px 5px;
                    font-size: 14px;
                    border-radius: 5px;
                    border: 1px solid #888;
                    background: #f0f0f0;
                    cursor: pointer;
                `;

                // 情報表示ボックス
                const infoBox = document.createElement('div');
                infoBox.style.cssText = `
                    background-color: #f5f5f5;
                    border: 1px solid #ccc;
                    width: 333px;
                    height: auto;
                    position: fixed;
                    top: 50px;
                    left: 0px;
                    z-index: 9999;
                    font-size: 18px;
                    line-height: 1.6;
                    color: #333;
                    padding: 15px;
                    overflow-y: auto;
                    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
                    border-radius: 8px;
                `;

                // 作者マイページボタン(存在する場合のみ追加)
                const authorButton = authorPageUrl ? `
                    <a href="${authorPageUrl}" target="_blank" style="
                        font-size: 14px;
                        text-align: center;
                    ">▶ 作者マイページへ</a>
                ` : '';

                infoBox.innerHTML = `
                    <strong>📚</strong> ${title}<br>
                    <strong>🖋️</strong> ${writer}<br>
                    ${authorButton}<br>
                    <div style="height: 10px;"></div>
                    <strong>📝</strong> ${genreText}<br>
                    <strong>🔑</strong> ${highlightedKeywords}<br>
                    <div style="height: 10px;"></div>
                    <strong>🔤 文字数:</strong> ${length}<br>
                    <strong>📖 全</strong> ${general_all_no}<br>
                    <strong>📅 最新掲載日:</strong> ${general_lastup}<br>
                    <strong>✍️ </strong> ${status} <strong style="color: red;">${eternal}</strong><br>
                `;

                toggleButton.onclick = () => {
                    infoBox.style.display = infoBox.style.display === 'none' ? 'block' : 'none';
                };

                document.body.appendChild(toggleButton);
                document.body.appendChild(infoBox);

            } catch (e) {
                console.error('JSON解析エラー:', e);
            }
        },
        onerror: function (err) {
            console.error('API通信エラー:', err);
        }
    });

})();

QingJ © 2025

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