Agregar enlace de portada de video a la página de detalles del video de Bilibili

Muestra un enlace directo a la portada del video en la página de detalles del video de Bilibili.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Bilibili视频详情页追加视频封面链接
// @name:zh-CN         Bilibili视频详情页追加视频封面链接
// @name:en      Add video cover link to Bilibili video detail page
// @name:ar          إضافة رابط صورة غلاف الفيديو إلى صفحة تفاصيل فيديو Bilibili
// @description:ar    يعرض رابطًا مباشرًا لصورة غلاف الفيديو في صفحة تفاصيل الفيديو على Bilibili.
// @name:bg          Добавяне на връзка към обложката на видеото към страницата с подробности за видеоклипа в Bilibili
// @description:bg    Показва директна връзка към обложката на видеото на страницата с подробности за видеоклипа в Bilibili.
// @name:cs          Přidat odkaz na obálku videa na stránku s podrobnostmi o videu Bilibili
// @description:cs    Zobrazí přímý odkaz na obálku videa na stránce s podrobnostmi o videu na Bilibili.
// @name:da          Tilføj videocoverlink til Bilibili-videodetaljeside
// @description:da    Viser et direkte link til videocoveret på Bilibilis videodetaljeside.
// @name:de          Video-Cover-Link zur Bilibili-Videodetailseite hinzufügen
// @description:de    Zeigt einen direkten Link zum Video-Cover auf der Bilibili-Videodetailseite an.
// @name:el          Προσθήκη συνδέσμου εξωφύλλου βίντεο στη σελίδα λεπτομερειών βίντεο Bilibili
// @description:el    Εμφανίζει έναν άμεσο σύνδεσμο προς το εξώφυλλο βίντεο στη σελίδα λεπτομερειών βίντεο του Bilibili.
// @name:eo          Aldoni videokovrilan ligilon al Bilibili-videa detala paĝo
// @description:eo    Montras rektan ligilon al la videokovrilo sur la Bilibili-videa detala paĝo.
// @name:es          Agregar enlace de portada de video a la página de detalles del video de Bilibili
// @description:es    Muestra un enlace directo a la portada del video en la página de detalles del video de Bilibili.
// @name:fi          Lisää videon kansikuvalinkki Bilibilin videotietosivulle
// @description:fi    Näyttää suoran linkin videon kansikuvaan Bilibilin videotietosivulla.
// @name:fr          Ajouter un lien de couverture vidéo à la page de détails de la vidéo Bilibili
// @description:fr    Affiche un lien direct vers la couverture de la vidéo sur la page de détails de la vidéo Bilibili.
// @name:fr-CA       Ajouter un lien de couverture vidéo à la page de détails de la vidéo Bilibili
// @description:fr-CA    Affiche un lien direct vers la couverture de la vidéo sur la page de détails de la vidéo Bilibili.
// @name:he          הוסף קישור לעטיפת וידאו לדף הפרטים של סרטון Bilibili
// @description:he    מציג קישור ישיר לעטיפת הוידאו בדף הפירוט של סרטון בביליבילי.
// @name:hr          Dodaj vezu naslovnice videa na stranicu s detaljima videozapisa Bilibili
// @description:hr    Prikazuje izravnu vezu na naslovnicu videa na stranici s detaljima videozapisa na Bilibili.
// @name:hu          Videóborító link hozzáadása a Bilibili videó részletező oldalához
// @description:hu    Közvetlen linket jelenít meg a videó borítójához a Bilibili videó részletező oldalán.
// @name:id          Tambahkan tautan sampul video ke halaman detail video Bilibili
// @description:id    Menampilkan tautan langsung ke sampul video di halaman detail video Bilibili.
// @name:it          Aggiungi link di copertina video alla pagina dei dettagli del video di Bilibili
// @description:it    Mostra un link diretto alla copertina del video nella pagina dei dettagli del video di Bilibili.
// @name:ja          Bilibiliビデオ詳細ページにビデオカバーリンクを追加
// @description:ja    Bilibiliのビデオ詳細ページにビデオカバーへの直接リンクを表示します。
// @name:ka          Bilibili ვიდეოს დეტალური გვერდზე ვიდეოს ყდის ბმულის დამატება
// @description:ka    აჩვენებს ვიდეოს ყდის პირდაპირ ბმულს Bilibili-ის ვიდეოს დეტალურ გვერდზე.
// @name:ko          Bilibili 비디오 세부 정보 페이지에 비디오 커버 링크 추가
// @description:ko    Bilibili 비디오 세부 정보 페이지에서 비디오 커버에 대한 직접 링크를 표시합니다.
// @name:nb          Legg til videocoverlenke til Bilibili videodetaljside
// @description:nb    Viser en direkte lenke til videocoveret på Bilibili videodetaljside.
// @name:nl          Voeg een video-coverlink toe aan de Bilibili-videodetailpagina
// @description:nl    Toont een directe link naar de video-cover op de Bilibili-videodetailpagina.
// @name:pl          Dodaj link do okładki wideo na stronie szczegółów filmu Bilibili
// @description:pl    Wyświetla bezpośredni link do okładki wideo na stronie szczegółów filmu Bilibili.
// @name:pt-BR       Adicionar link da capa do vídeo à página de detalhes do vídeo Bilibili
// @description:pt-BR    Exibe um link direto para a capa do vídeo na página de detalhes do vídeo no Bilibili.
// @name:ro          Adăugați un link de copertă video la pagina de detalii video Bilibili
// @description:ro    Afișează un link direct către coperta video pe pagina de detalii video Bilibili.
// @name:ru          Добавить ссылку на обложку видео на страницу сведений о видео Bilibili
// @description:ru    Отображает прямую ссылку на обложку видео на странице сведений о видео Bilibili.
// @name:sk          Pridať odkaz na obálku videa na stránku s podrobnosťami o videu Bilibili
// @description:sk    Zobrazuje priamy odkaz na obálku videa na stránke s podrobnosťami o videu na Bilibili.
// @name:sr          Додај везу омота видео снимка на страницу са детаљима видео снимка Bilibili
// @description:sr    Приказује директну везу до омота видео снимка на страници са детаљима видео снимка на Bilibili.
// @name:sv          Lägg till videocoverlänk till Bilibili videodetaljsida
// @description:sv    Visar en direktlänk till videocoveret på Bilibili videodetaljsida.
// @name:th          เพิ่มลิงก์หน้าปกวิดีโอไปยังหน้าข้อมูลวิดีโอ Bilibili
// @description:th    แสดงลิงก์ตรงไปยังหน้าปกวิดีโอบนหน้าข้อมูลวิดีโอของ Bilibili
// @name:tr          Bilibili video ayrıntı sayfasına video kapak bağlantısı ekle
// @description:tr    Bilibili video ayrıntı sayfasında video kapağına doğrudan bağlantı görüntüler.
// @name:ug          Bilibili سىن تەپسىلىي بېتىگە سىن مۇقاۋىسى ئۇلىنىشى قوشۇڭ
// @description:ug    Bilibili سىن تەپسىلىي بېتىدە سىن مۇقاۋىسىغا بىۋاسىتە ئۇلىنىش كۆرسىتىدۇ.
// @name:uk          Додати посилання на обкладинку відео на сторінку відомостей про відео Bilibili
// @description:uk    Відображає пряме посилання на обкладинку відео на сторінці відомостей про відео Bilibili.
// @name:vi          Thêm liên kết bìa video vào trang chi tiết video Bilibili
// @description:vi    Hiển thị một liên kết trực tiếp đến ảnh bìa video trên trang chi tiết video Bilibili.
// @name:zh          Bilibili视频详情页追加视频封面链接
// @description:zh    在Bilibili视频详情页面显示视频封面图片的直接链接。
// @description:zh-CN    在Bilibili视频详情页面显示视频封面图片的直接链接。
// @name:zh-HK       Bilibili視頻詳情頁追加視頻封面鏈接
// @description:zh-HK    在Bilibili視頻詳情頁面顯示視頻封面圖片的直接鏈接。
// @name:zh-SG       Bilibili视频详情页追加视频封面链接
// @description:zh-SG    在Bilibili视频详情页面显示视频封面图片的直接链接。
// @name:zh-TW       Bilibili視頻詳情頁追加視頻封面鏈接
// @description:zh-TW    在Bilibili視頻詳情頁面顯示視頻封面圖片的直接鏈接。
// @namespace    http://tampermonkey.net/
// @version      0.1.0
// @description  在视频详情页追加视频封面链接
// @description:en  Add video cover link to video detail page
// @namespace    http://tampermonkey.net/
// @description  Adds a link to the video cover in the info section and displays the cover image below the recommendations footer on Bilibili video pages.
// @author       aspen138
// @match        https://www.bilibili.com/video/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_notification
// @grant        window.onurlchange
// @license      MIT
// ==/UserScript==


// Acknowledgement: Gemini 2.5 Pro 03-25


(function() {
    'use strict';

    // ↓↓↓↓↓↓↓↓↓模板,建议直接复制 //

    // 自定义 urlchange 事件(用来监听 URL 变化)
    function addUrlChangeEvent() {
        if (window.onurlchange === undefined) { // Only add if Tampermonkey hasn't provided it
            history.pushState = ( f => function pushState(){
                var ret = f.apply(this, arguments);
                window.dispatchEvent(new Event('pushstate'));
                window.dispatchEvent(new Event('urlchange'));
                return ret;
            })(history.pushState);

            history.replaceState = ( f => function replaceState(){
                var ret = f.apply(this, arguments);
                window.dispatchEvent(new Event('replacestate'));
                window.dispatchEvent(new Event('urlchange'));
                return ret;
            })(history.replaceState);

            window.addEventListener('popstate',()=>{
                window.dispatchEvent(new Event('urlchange'))
            });
            console.log("Custom 'urlchange' event listener added.");
        } else {
             console.log("Using built-in 'urlchange' event listener.");
        }
    }


    var menu_ALL = [
        ['menu_isEnableAppendCover', '添加视频封面链接和图片', '添加视频封面链接和图片功能', true] // Default changed to true for convenience
    ], menu_ID = [];

    // Initialize default values
    for (let i=0; i<menu_ALL.length; i++){
        if (GM_getValue(menu_ALL[i][0]) === null){ // Use strict comparison for null
            GM_setValue(menu_ALL[i][0], menu_ALL[i][3]);
        }
    }

    // 注册脚本菜单
    function registerMenuCommand() {
        // Clear existing menus before re-registering
        for (let i = 0; i < menu_ID.length; i++) {
            if (menu_ID[i]) { // Check if ID exists before trying to unregister
                 try {
                    GM_unregisterMenuCommand(menu_ID[i]);
                 } catch (e) {
                    console.warn("Could not unregister menu command:", menu_ID[i], e);
                 }
            }
        }
        menu_ID = []; // Reset the array

        for (let i = 0; i < menu_ALL.length; i++) {
            menu_ALL[i][3] = GM_getValue(menu_ALL[i][0]); // Update current status from storage
            const isEnabled = menu_ALL[i][3];
            const commandLabel = `${isEnabled ? '✅' : '❌'} ${menu_ALL[i][1]}`;
            const menuName = menu_ALL[i][0];
            const menuTips = menu_ALL[i][2];

             // Use a closure to capture the correct variables for the callback
            (function(currentStatus, name, tips) {
                menu_ID[i] = GM_registerMenuCommand(commandLabel, function() {
                    menu_switch(currentStatus, name, tips);
                });
            })(isEnabled, menuName, menuTips);
        }
    }

    // 菜单开关
    function menu_switch(menu_status, Name, Tips) {
        const newState = !menu_status; // Toggle the state
        GM_setValue(Name, newState);
        GM_notification({
            text: `已${newState ? '开启' : '关闭'} [${Tips}] 功能\n(刷新网页后生效)`, // Simplified message
            timeout: 3500,
            onclick: function(){ location.reload(); }
        });
        registerMenuCommand(); // Update menu state immediately
    };

    // ↑↑↑↑↑↑↑↑↑↑↑↑模板,建议直接复制 //

    // --- Script Core Logic ---

    const INFO_CONTAINER_SELECTOR = '.video-info-detail-list.video-info-detail-content';
    const FOOTER_SELECTOR = '.rec-footer[data-v-17ce950e]'; // More specific selector if needed
    const COVER_LINK_CLASS = 'bili-cover-link-item'; // Custom class for the link container
    const COVER_IMAGE_CONTAINER_CLASS = 'bili-cover-image-container'; // Custom class for the image container

    let currentBVid = null; // Keep track of the current video ID
    let checkInterval = null; // Interval timer handle
    const CHECK_INTERVAL_MS = 500; // Check every 500ms
    const MAX_CHECKS = 40; // Try for 20 seconds max

    // Main function to add cover link and image
    function addCoverElements() {
        const infoContainer = document.querySelector(INFO_CONTAINER_SELECTOR);
        const recFooter = document.querySelector(FOOTER_SELECTOR);
        const imageMetaTag = document.head.querySelector('meta[itemprop="image"]');

        // Check if all necessary elements are present
        if (!infoContainer || !recFooter || !imageMetaTag) {
            // console.log('Waiting for elements...');
            return false; // Indicate elements are not ready
        }

        // Check if we've already processed this specific info container
        if (infoContainer.dataset.coverProcessed === 'true') {
            // console.log('Already processed this container.');
            return true; // Indicate processing is done or already happened
        }

        // Extract cover URL
        const coverImgUrlRaw = imageMetaTag.getAttribute('content');
        if (!coverImgUrlRaw) {
            console.warn('Cover image meta tag found, but content is empty.');
            return false; // Cannot proceed without URL
        }
        const coverImgUrl = 'https://' + coverImgUrlRaw.replace(/^https?:?\/\//, '').split('@')[0];
        console.log("Cover URL found:", coverImgUrl);

        // --- 1. Add the Cover Link to Info Section ---
        // Check if link already exists (belt-and-suspenders check)
        if (!infoContainer.querySelector(`.${COVER_LINK_CLASS}`)) {
            const coverItem = document.createElement('div');
            coverItem.classList.add(COVER_LINK_CLASS, 'item'); // Add custom class and 'item' class

            // Optional: Add an icon (simplified)
            const coverIcon = document.createElement('span');
            coverIcon.textContent = '🖼️'; // Emoji icon
            coverIcon.style.marginRight = '5px';
            coverIcon.style.fontSize = '16px';
            coverIcon.style.verticalAlign = 'middle';

            const coverLink = document.createElement('a');
            coverLink.href = coverImgUrl;
            coverLink.target = '_blank';
            coverLink.rel = 'noopener noreferrer';
            coverLink.title = '点击查看封面原图 (Click to view original cover)';
            coverLink.style.verticalAlign = 'middle';

            const linkText = document.createElement('span');
            linkText.textContent = '封面 (Cover)';

            coverLink.appendChild(coverIcon);
            coverLink.appendChild(linkText);
            coverItem.appendChild(coverLink);

            infoContainer.appendChild(coverItem); // Append the new item
            console.log('Cover link appended to info section.');
        }

        // --- 2. Add the Cover Image below the Footer ---
        // Check if image container already exists as the next sibling of the footer
        if (!recFooter.nextElementSibling || !recFooter.nextElementSibling.classList.contains(COVER_IMAGE_CONTAINER_CLASS)) {
            const imageContainer = document.createElement('div');
            imageContainer.className = COVER_IMAGE_CONTAINER_CLASS;
            imageContainer.style.marginTop = '15px'; // Add some space above the image
            imageContainer.style.textAlign = 'center'; // Center the image container

            const coverImage = document.createElement('img');
            coverImage.src = coverImgUrl;
            coverImage.alt = 'Video Cover Image';
            coverImage.style.maxWidth = '100%'; // Ensure image is responsive
            coverImage.style.height = 'auto';
            coverImage.style.borderRadius = '4px'; // Optional: slightly rounded corners
            coverImage.style.cursor = 'pointer'; // Indicate it's clickable
            coverImage.title = '点击查看封面原图 (Click to view original cover)';

            // Make the image itself a link to the cover
            const imageLink = document.createElement('a');
            imageLink.href = coverImgUrl;
            imageLink.target = '_blank';
            imageLink.rel = 'noopener noreferrer';
            imageLink.appendChild(coverImage);

            imageContainer.appendChild(imageLink);

            // Insert the container *after* the footer element
            recFooter.parentNode.insertBefore(imageContainer, recFooter.nextSibling);
            console.log('Cover image appended below footer.');
        }

        // Mark the info container as processed to prevent duplicates if this function runs again
        infoContainer.dataset.coverProcessed = 'true';

        return true; // Indicate success
    }

    // Function to start the process of adding elements
    function runLogic() {
        if (!GM_getValue('menu_isEnableAppendCover', true)) {
            console.log('Cover feature is disabled.');
            return; // Exit if the feature is turned off
        }

        const newBVid = location.pathname.match(/BV[^/]+/)?.[0];
        if (!newBVid) {
            // console.log("Not a video page or BVid not found.");
            return;
        }

        // Only reset and run if the BVid changed or it's the first time
        if (newBVid !== currentBVid) {
            console.log(`New video detected (BVid: ${newBVid}). Running cover logic.`);
            currentBVid = newBVid;

            // Clear any previous interval
            if (checkInterval) {
                clearInterval(checkInterval);
                checkInterval = null;
            }

            // Reset processed flags on potential old elements (though ideally elements are replaced on navigation)
             const oldInfo = document.querySelector(`${INFO_CONTAINER_SELECTOR}[data-cover-processed="true"]`);
             if(oldInfo) delete oldInfo.dataset.coverProcessed;
             const oldImage = document.querySelector(`.${COVER_IMAGE_CONTAINER_CLASS}`);
             if(oldImage) oldImage.remove();


            let checks = 0;
            checkInterval = setInterval(() => {
                checks++;
                // console.log(`Check ${checks}/${MAX_CHECKS}`);
                try {
                    if (addCoverElements() || checks >= MAX_CHECKS) {
                        clearInterval(checkInterval);
                        checkInterval = null;
                        if (checks >= MAX_CHECKS) {
                             console.warn("Cover script timed out waiting for elements.");
                        } else {
                             console.log("Cover elements added successfully or already present.");
                        }
                    }
                } catch (error) {
                    console.error("Error during cover element check/addition:", error);
                    clearInterval(checkInterval); // Stop on error
                    checkInterval = null;
                }
            }, CHECK_INTERVAL_MS);
        } else {
             // console.log("Same video page (BVid: " + currentBVid + "), no immediate action needed unless elements reappear.");
             // Optional: Could add a check here to see if elements disappeared and need re-adding,
             // but usually page navigation handles this.
        }
    }

    // --- Initialization ---
    addUrlChangeEvent(); // Ensure URL change listener is set up
    registerMenuCommand(); // Set up the script menu

    // Run the logic on initial load
    // Use DOMContentLoaded or a small delay to ensure basic structure exists
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', runLogic);
    } else {
        // Small delay in case runLogic depends on elements added dynamically shortly after load
        setTimeout(runLogic, 200);
    }

    // Run the logic whenever the URL changes
    window.addEventListener('urlchange', () => {
        console.log("URL change detected by script.");
        // Use a small delay to allow the page content to potentially update
        setTimeout(runLogic, 500); // Delay slightly after URL change
    });

})();