Piped Video Previews

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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);
});