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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
    }
})();