Piped Video Previews

Displays an animated video preview when hovering over its thumbnail on Piped websites

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Piped Video Previews
// @name:ru      Piped Video Previews
// @namespace    VideoPreviews
// @version      1.1
// @description  Displays an animated video preview when hovering over its thumbnail on Piped websites
// @description:ru  Показывает анимированное превью видео при наведении курсора на его миниатюру на сайтах Piped
// @author       SearchDL
// @match        *://piped.video/*
// @match        *://*.piped.video/*
// @match        *://piped.kavin.rocks/*
// @match        *://piped.yt/*
// @icon         https://piped.video/favicon.ico
// @license      MIT
// @grant        none
// ==/UserScript==

// Settings
var ThumbChangingSpeedMs = 250; // Time in milliseconds before frame change during preview | Время в мс до смены кадра во время предпросмотра
var DelayForListViewMs = 0; // Delay before requesting preview frames when hovering over a thumbnail for videos displayed in 1 column (related videos) | Задержка до запроса эскизов при наведении на миниатюру для видео, отображающихся в 1 ряд (похожие видео)
var DelayForGridViewMs = 300; // Delay before requesting preview frames for grid-displayed videos (channel and playlist videos, watch history, search results) | Задержка до запроса эскизов для видео, отображающихся сеткой (видео канала и плейлиста, история просмотра, результаты поиска)
var FallbackApiUrl = "https://pipedapi.kavin.rocks" // Piped instance API URL used when it is impossible to get the current API URL from the page: in watch history, in search results and in trends | API-адрес зеркала Piped, используемый в случае, когда нельзя получить текущий API-адрес со страницы: в истории просмотра, в поиске и в трендах
// List of instances API URLs: https://github.com/TeamPiped/Piped/wiki/Instances





var prevbox;
var canvas;
var hovered = false;
var timeout;
var finished = false;
var apiurl = FallbackApiUrl;

function getApiUrl(t) {
    var rss = t.querySelector('i.i-fa6-solid\\:rss');
    if (rss) {
        var url = new URL(rss.parentNode.href);
        apiurl = url.protocol + "//" + url.host;
    }
}

function updatePreviewBoxes(t) {
    var boxes = t.querySelectorAll(".aspect-video.w-full.object-contain");
    boxes.forEach(
        function(cbox) {
            cbox.addEventListener("mouseover", thumbnailIn, false);
            cbox.addEventListener("mouseout", thumbnailOut, false);
        }
    );
}

(function() {
    'use strict';

    getApiUrl(document);
    updatePreviewBoxes(document);
})();

var observer = new MutationObserver(function(mutations){
    mutations.forEach(function(mutation){
        getApiUrl(mutation.target);
        updatePreviewBoxes(mutation.target);
    });
});
observer.observe(document.body, {childList:true,subtree:true});


function thumbnailIn() {
    if (finished) {
        finished = false;
        return;
    }

    if (hovered && prevbox) {
        restore(prevbox);
    }
    var url = this.parentNode.parentNode.attributes.href.value;
    prevbox = this;
    hovered = true;
    if (!url.includes("watch?v=")) return;

    if (window.location.href.includes("watch?v=") && !url.includes("list=")) timeout = setTimeout(() => processThumbnails(this), DelayForListViewMs);
    else timeout = setTimeout(() => processThumbnails(this), DelayForGridViewMs);
}

function processThumbnails(box) {
    var url = box.parentNode.parentNode.attributes.href.value;
    box.style.opacity = '0.5';
    box.style.transition = 'opacity 0.5s ease-in-out';

    fetchData(apiurl + "/streams/" + url.substring(url.indexOf("=") + 1)).then(data => {
        if (prevbox.src !== box.src) {
            return;
        }

        if (hovered) {
            if (!data || data.previewFrames.length < 1) {
                box.style.border = '2px solid';
                box.style.borderColor = 'red';
                return;
            }

            var maxn = 0;
            var maxh = 0;
            for (let i = 0; i < data.previewFrames.length; i++) {
                if (data.previewFrames[i].frameHeight > maxh) {
                    maxn = i;
                    maxh = data.previewFrames[i].frameHeight;
                }
            }
            var frames = data.previewFrames[maxn];
            var img, next;
            canvas = document.createElement('canvas');
            var ctx = canvas.getContext('2d');
            canvas.width = parseInt(box.width);
            canvas.height = parseInt(box.height);

            function changeImage(i, y, x) {
                if (!hovered) return;

                var X = frames.framesPerPageX;
                var Y = frames.framesPerPageY;
                if (i * X*Y + y * X + x >= frames.totalCount - 1) {
                    finished = true;
                    canvas.replaceWith(box); // эта замена вызывает события выхода и захода курсора в область превью
                    return;
                }

                if (i < 0 || (y == Y-1 && x == X-1)) {
                    i++;
                    x = 0;
                    y = 0;
                    img = new Image();
                    next = new Image();
                    img.src = frames.urls[i];
                    if (i < frames.urls.length - 1) {
                        next.src = frames.urls[i + 1]; // предзагрузка следующего атласа миниатюр
                    }
                }
                else if (x == X-1) {
                    y++;
                    x = 0;
                }
                else x++;

                if (!img.complete || img.naturalWidth == 0) {
                    timeout = setTimeout(() => changeImage(i, y, x-1), 50);
                }
                else {
                    var sx = x * frames.frameWidth;
                    var sy = y * frames.frameHeight;
                    var sw = frames.frameWidth;
                    var sh = frames.frameHeight;
                    var scaleX = canvas.width / sw;
                    var scaleY = canvas.height / sh;
                    var scale = Math.min(scaleX, scaleY);
                    var offsetX = (canvas.width - sw*scale) / 2;
                    var offsetY = (canvas.height - sh*scale) / 2;
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(img, sx, sy, sw, sh, offsetX, offsetY, sw*scale, sh*scale);

                    box.replaceWith(canvas);

                    canvas.onmouseleave = () => {
                        restore(box);
                    };

                    timeout = setTimeout(() => changeImage(i, y, x), ThumbChangingSpeedMs);
                }
            }

            changeImage(-1, 0, 0);
        }
    });
}

function thumbnailOut() {
    if (!finished) restore(this);
}

function restore(box) {
    hovered = false;
    box.style.opacity = '';
    box.style.transition = '';
    box.style.border = '';
    box.style.borderColor = '';
    clearTimeout(timeout);
    if (canvas) canvas.replaceWith(box);
}

async function fetchData(url) {
    var response = await fetch(url);
    if (!response.ok) {
        return "";
    }

    var data = await response.json();
    return data;
}

const originalReplaceState = history.replaceState;
history.replaceState = function () { // переход по страницам на сайте
    originalReplaceState.apply(this, arguments);
    if (hovered) restore(prevbox);
}
window.addEventListener('popstate', function(event) { // навигация по истории браузера вручную
    if (hovered) restore(prevbox);
});