Nyaa MyAnimeList Search Button

Adds a quick MyAnimeList (MAL) search button to Nyaa.si posts (during search and inside posts) for Anime, Manga, and Light Novels.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Nyaa MyAnimeList Search Button
// @namespace    https://greasyfork.org/users/whitewriter
// @version      1.3
// @description  Adds a quick MyAnimeList (MAL) search button to Nyaa.si posts (during search and inside posts) for Anime, Manga, and Light Novels.
// @author       WhiteWriter
// @match        https://nyaa.si/*
// @license      MIT
// @icon         https://nyaa.si/static/favicon.png
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Check if we're inside a post (/view/ in the url) or the main/search page
    const isViewPage = window.location.pathname.startsWith('/view/');

    if (isViewPage) {
        // Handle individual post page
        processViewPage();
    } else {
        // Handle main/search page
        processMainPage();
    }

    // Function to process main/search page
    function processMainPage() {
        // Select all post rows (any <tr> inside <tbody>)
        const rows = document.querySelectorAll('tbody tr');
        rows.forEach(row => {
            // Get category from first <td>
            const categoryTd = row.children[0];
            if (!categoryTd) return;
            const categoryA = categoryTd.querySelector('a');
            if (!categoryA) return;
            const categoryTitle = categoryA.getAttribute('title').toLowerCase();

            // Determine contentType (literature counts for both manga and light novel)
            let contentType;
            if (categoryTitle.includes('anime')) {
                contentType = 'anime';
            } else if (categoryTitle.includes('literature')) {
                contentType = 'manga';
            }
            if (!contentType) return;

            // Get title from second <td>, skipping comments if present
            const titleTd = row.children[1];
            if (!titleTd) return;
            const titleA = Array.from(titleTd.querySelectorAll('a')).find(a => !a.classList.contains('comments'));
            if (!titleA) return;
            const title = titleA.getAttribute('title');

            // Clean title and proceed. contentName is the best approximation to the
            // content's title in order to search on MAL (doesn't need to be exact)
            const contentName = cleanTitle(title);
            if (contentName) {
                addMalButton(row.children[2], contentType, contentName);
            }
        });
    }

    // Function to process individual view page (inside a post)
    function processViewPage() {
        // Get title from <h3 class="panel-title">
        const titleElement = document.querySelector('div.panel-heading h3.panel-title');
        if (!titleElement) return;
        const title = titleElement.textContent;

        // Get category from <div class="col-md-5"> <a>
        const categoryElement = document.querySelector('div.panel-body div.row div.col-md-5 a');
        if (!categoryElement) return;
        const categoryText = categoryElement.textContent.toLowerCase();

        // Determine contentType
        let contentType;
        if (categoryText.includes('anime')) {
            contentType = 'anime';
        } else if (categoryText.includes('literature')) {
            contentType = 'manga';
        }
        if (!contentType) return;

        // Clean title
        const contentName = cleanTitle(title);
        if (contentName) {
            // Add button to panel-footer
            const footer = document.querySelector('div.panel-footer.clearfix');
            if (footer) {
                addMalButton(footer, contentType, contentName);
            }
        }
    }

    // Function to add the MAL button
    function addMalButton(container, contentType, contentName) {
        const baseUrl = contentType === 'anime'
            ? 'https://myanimelist.net/anime.php'
            : 'https://myanimelist.net/manga.php';
        const params = new URLSearchParams();
        params.append('q', contentName);
        const malUrl = `${baseUrl}?${params.toString()}`;

        const buttonHtml = `<a href="${malUrl}" target="_blank" style="display: inline-block; padding: 5px 10px; background-color: #337ab7; color: white; text-decoration: none; border-radius: 3px; margin-left: 5px;">MAL</a>`;
        container.insertAdjacentHTML('beforeend', buttonHtml);
    }

    // Function to clean the title and extract contentName
    function cleanTitle(title) {
        // Remove text within brackets (may sacrifice the button on posts with the content title within brackets)
        let cleaned = title
            .replace(/\[.*?\]/g, '')
            .replace(/\([^()]*?\)/g, '')
            .replace(/\{.*?\}/g, '');

        // Remove specific keywords with optional punctuation
        const keywords = [
            '1080p', '2160p', '720p', '480p', '360p', 'Multi-Audio', 'Multi Audio', '10bit',
            'AV1', 'MP4', 'AAC', 'EAC3', 'E-AC3', 'AC3', 'DTS', 'DTS-HD', 'UHD', 'HDR',
            'English Dub', 'Dual-Audio', 'Dual Audio', 'x264', 'x265', 'h.264', 'h.265',
            'Opus', 'AVI', 'WMV', 'VFVOSTFR', 'BDRip', 'BluRay', 'BD', 'WEB', 'Eng Sub',
            'Subbed', 'FLAC', '10-bit', 'Batch', 'HD', 'HorribleSubs', 'Horrible-Subs',
            'Multi-Subs', 'VOSTFR', 'FLAC2.0', 'FLAC5.1', 'FLAC7.1', 'MPEG', 'WebRip',
            'HEVC', '8bit', 'Web-DL', 'AAC2.0', 'AAC5.1', 'Multi-Sub',
            'Multi Audio', 'CR', 'DDP'
        ];
        const regex = new RegExp(
            '(?<!\\w)(?:' + keywords.join('|') + ')(?:[.,|-])?(?!\\w)',
            'gi'
        );
        cleaned = cleaned.replace(regex, '');

        // Trim and normalize spaces, remove leftover punctuation
        cleaned = cleaned
            .trim()
            .replace(/\s+/g, ' ')
            .replace(/^[.,|-]+|[.,|-]+$/g, '');

        return cleaned;
    }
})();