YouTube Video Preview and Ratings Keyless

Instant video previews in popup player by hovering or clicking video thumbs. Video ratings and resolution data shown on the thumbs. Gets video information without using a YouTube API key, which can be banned or limited.

// ==UserScript==
// @name         YouTube Video Preview and Ratings Keyless
// @namespace    YouTubeVideoPreviewPlayer
// @version      20230327
// @description  Instant video previews in popup player by hovering or clicking video thumbs. Video ratings and resolution data shown on the thumbs. Gets video information without using a YouTube API key, which can be banned or limited.
// @author       Couchy
// @match        https://www.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @noframes
// ==/UserScript==

//==================================================================
//Userscript specific functions

function debug(...args) {
    //console.log(...args);
}

function set_pref(preference, new_value) {
    GM_setValue(preference, new_value);
}

function get_pref(preference) {
    return GM_getValue(preference);
}

function init_pref(preference, new_value) {
    let value = get_pref(preference);
    if (value == null) {
        set_pref(preference, new_value);
        value = new_value;
    }
    return value;
}

//==================================================================
// Styles

const style_basic = `
/* prefs */
#vpp_pref_popup {direction:ltr; font:11px/11px Roboto,arial,sans-serif; position:fixed; right:0px; top:0px; color:#e0e0e0; background:#202020; padding:15px 15px 15px 10px; border-radius:3px; box-shadow:0px 0px 5px 1px gray; /*z-index:2147483647;*/ z-index:2147483646;}
.vpp_pref_group {margin-left:15px; color:yellow;}
#vpp_pref_close {font:14px/14px Roboto,arial,sans-serif; color:lightgray; position:absolute; top:3px; right:5px; cursor:pointer; user-select:none; -moz-user-select:none;}
#vpp_pref_close:hover {color:white;}
#vpp_pref_title {font:500 13px/13px Roboto,arial,sans-serif; padding:5px !important;}
#vpp_pref_button {cursor:pointer; width:18px; height:18px; background-size:contain; background-repeat:no-repeat; opacity:0.7; position:absolute; right:0px; top:0px; user-select:none; -moz-user-select:none;}
#vpp_pref_button:hover {opacity:1;}
#vpp_pref_button {background-image:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAeUlEQVR42u2UwQ2AMAwDk52YCWaCmdgp7QshKP6QKn7Yv0pudEqcuBXLBXB/7GaBzFv3Z3k+AdYY/z3cr+IZHgywPCZzxrv4T48AyAFAeKaH0ICmr2GFuDpAkQGOLRBAOQAIjw5Rtrg6QJEBji0QQDkACI8OUbYE0ADbonowcNn6sgAAAABJRU5ErkJggg==');}
#vpp_pref_delay_text {margin-left:20px; color:khaki;}
#vpp_pref_delay_num {margin-left:3px; color:khaki;}
.vpp_pref_delay_plusminus {margin-left:5px; cursor:pointer; text-align:center; font-weight:800; color:black; background:#B0B0B0; border-radius:2px; display:inline-block; width:10px; user-select:none; -moz-user-select:none;}
.vpp_pref_delay_plusminus:hover {background:#D0D0D0;}
#vpp_pref_delay_plus {margin-left:2px;}
#vpp_pref_delay_minus {margin-left:5px;}
/* meta data */
.vpp_meta_def_container {font:500 12px/14px Roboto,arial,sans-serif; position:absolute; top:0px; left:0px; background:#e8e8e8; padding:0px 3px; border-radius:2px; display:none; cursor:default;}
html[dark] .vpp_meta_def_container {background:#202020 !important;}
.vpp_meta_def_container[reveal] {display: block;}
.vpp_meta_def_container[space] .vpp_meta_def_hd {margin-right:2px;}
.vpp_meta_def_format {position:relative; color:black;}
 html[dark] .vpp_meta_def_format {color:#f0f0f0 !important;}
.vpp_meta_def_hd {position:relative;}
.vpp_meta_def_hd.HD {color:magenta;}
.vpp_meta_def_hd.UHD {color:red;}
.vpp_meta_rate {direction:ltr; font:500 12px/14px Roboto,arial,sans-serif; position:absolute; top:0px; right:0px; color:white; padding:0px 3px; border-radius:2px; cursor:default;}
body[dir='ltr'] [vpp_meta_rate] ytd-thumbnail-overlay-toggle-button-renderer, body[dir='rtl'] [vpp_meta_def] ytd-thumbnail-overlay-toggle-button-renderer {margin-top:12px !important;}
#vpp_meta_box {position:relative; float:left; height:13px; margin-top:3px;}
#vpp_meta_box .vpp_meta_def_container {background:none !important; position:relative; clear:none; float:left;}
#vpp_meta_box .vpp_meta_rate {position:relative; clear:none; float:right; margin-left:5px;}
#gridtube_title_container {position:absolute; top:5px; right:5px;}
body[dir='rtl'] #gridtube_title_container {left:5px !important; right:auto !important;}
body ytd-video-primary-info-renderer {position:relative !important;}
/* play button*/
#vpp_now_playing {font:500 14px/14px Roboto,arial,sans-serif; position:absolute; bottom:0px; left:0px; background:red; color:white; padding:5px; cursor:default; z-index:0;}
body[dir='rtl'] #vpp_now_playing {left:auto !important; right:0px !important;}
body[vpp_reveal_play_button] .vpp_play_button, *[vpp_play_marked]:hover .vpp_play_button {visibility:visible !important;}
.vpp_play_button_container {position:absolute; bottom:0px; left:0px; width:100%;}
.vpp_play_button {display:block !important; position:relative !important; padding-bottom:1px !important; margin:0px auto !important; width:25px !important; height:25px !important;
                  opacity:0.75 !important; cursor:pointer !important; background-size:25px !important; background-repeat:no-repeat !important; text-decoration:none !important; z-index:1; user-select:none; -moz-user-select:none;}
.vpp_play_button:hover {opacity:1 !important;}
.vpp_play_button {background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAG7AAABuwBHnU4NQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOqSURBVFiF5ZdPSBxnGMZ/M98s1V1nV01A17VZUwMRqsGY9FyQlBJQ2gYPIbaB0nMgwYZWeumxIQhlT7aQW0Mhl54CvQQiISQhkixN\
bWKXaNSaYuzu6v4Z98+38/WgY9Vu9l835NAHvsvM983zvs/7vPPOaEopXif018oOGAAfjY4GgEvAu0DnK+b8A5gCvvjp6tVl7cMzZ94EwkArgM/n42BXF7peX3Fs22b+2TPW19edSzGg3wAmgFbTNPlybIyDBw7UlXgv5hcX+WZigmQy2QpMiJ6+vhDQ9Pn587wVDGLb9itdXtOku7ubW7dvA+wzgHa3201XMEg2nyebzyOlpN69oQGG\
YfCGy0VXMIjb7cayrHYDoDMQIJPLkUynKdh2nal3Q+g6psdDZyDA75HIZhcoIJFKIQuFkofz2SxCCHTDqDkA27ZJpFLbCusAuVyOvJQopUquWDzO5OQkfy4vl91bauWlJJfL/ROAlLJiEy0sLfFtKMTP16+TsayazSilBLZeRI405aBsm8JWmW7cvMn0gweMnDrFocOHay7JpgeUqiiAQqGwHQBALB7n+ytXONLXx/DQEKbPVzGxM4Oq\
U0CpXQE4eBgOMzMzwwfDwwwcO1aVSatSwN5Rgr1TVErJj9eucefuXUZGRmjz+8smAzumYUXmKRSQUiKl3C7H3vV0fp5Lly9za2oKrcRzdylQLKOiCkDREuzFe4ODfDo6yloqxUo0WnJvzSUohg6/n/GxMXp6eogsLJDd6vViqM2ELwlACMHHp0/z2dmzLK2s8MvsbNlnOai6DZ0XiIMjvb18PT6Ox+vl4ZMnFZXI4dwOAKpvwyaPhwvn\
zjF08iSzc3Msrq5WRLwX28OoGg+8f+IEX128SMKyuBcO1zS6nTObClRYAndjI9+FQvT29vJbJMJGNlsDtRPBjhLouo6tFFqZM/v278dsamL60aPaidnM3vnmNIQQaEKwYVk0NDSUPJhMp0mm0/+JHCCTyaALgcswMLymqWwptfVEgkwmg9vtRgiBppXTozo4BrYsi2wuh7JtvF6vMjra21+sRqNtqVSKtKYRi8frSvyyYDSgw+9/YRw/\
evTpnfv329aiUQyXC+qceRF2lG3T0tzM8f7+OW3m8eP++NrajXvT061SSqRSZb8Ny6FoCpqGEIIGw8DlcvHOwEC82ecb1JRS/BWLvZ1MJn/I5/N+lGpUOwxQSo+SPtm6968dmrbhEuK51zQ/aWlp+VX73/8d/w3y7NP9Di2fPgAAAABJRU5ErkJggg==') !important;}
/* preview player */
#vpp_player_area {position:fixed; width:100%; height:100%; top:0px; left:0px; background:rgba(0,0,0,0.5); overflow:hidden !important; z-index:2147483646 !important;}
#vpp_player_area2 {position:relative; width:100%; height:100%; visibility:hidden;}
#vpp_player_box {position:absolute; background:#606060; box-shadow:0px 0px 8px 3px rgba(128,128,128,0.9); border-radius:5px 5px 0px 0px; max-width:100%; max-height:100%;}
#vpp_player_box[player_pos='00']:not([player_size='fit']) {top:0px; left:0px;}
#vpp_player_box[player_pos='01']:not([player_size='fit']) {top:0px; left:0px; right:0px; margin:auto;}
#vpp_player_box[player_pos='02']:not([player_size='fit']) {top:0px; right:0px;}
#vpp_player_box[player_pos='10']:not([player_size='fit']) {top:0px; bottom:0px; left:0px; margin:auto;}
#vpp_player_box[player_pos='11']:not([player_size='fit']) {top:0px; bottom:0px; left:0px; right:0px; margin:auto;}
#vpp_player_box[player_pos='12']:not([player_size='fit']) {top:0px; bottom:0px; right:0px; margin:auto;}
#vpp_player_box[player_pos='20']:not([player_size='fit']) {bottom:0px; left:0px;}
#vpp_player_box[player_pos='21']:not([player_size='fit']) {bottom:0px; left:0px; right:0px; margin:auto;}
#vpp_player_box[player_pos='22']:not([player_size='fit']) {bottom:0px; right:0px;}
#vpp_player_box[player_pos='right']:not([player_size='fit']) {float:right;}
#vpp_player_box[player_size='xxsmall'] {/*320x180*/ width:320px; height:200px;}
#vpp_player_box[player_size='xsmall'] {/*512x288*/ width:512px; height:308px;}
#vpp_player_box[player_size='small'] {/*768x432*/ width:768px; height:452px;}
#vpp_player_box[player_size='medium'] {/*1024x576*/  width:1024px; height:596px;}
#vpp_player_box[player_size='large'] {/*1280x720*/ width:1280px; height:740px;}
#vpp_player_box[player_size='xlarge'] {/*1600x900*/ width:1600px; height:920px;}
#vpp_player_box[player_size='xxlarge'] {/*1920x1080*/ width:1920px; height:1100px;}
#vpp_player_box[player_size='fit'] {width:100%; height:100%; border-radius:0px !important;}
#vpp_player_holder {position:relative; width:100%; height:100%;}
#vpp_player_holder2 {position:absolute; top:20px; left:0px; right:0px; bottom:0px; margin:auto; background:black;}
#vpp_player_box[player_size='fit'] #vpp_player_holder2 {left:0px !important; right:0px !important;}
#vpp_player_frame {position:relative; width:100%; height:100%; display:block; border:0px;}
#vpp_player_button_area_top {direction:ltr; font:500 14px/20px Roboto,arial,sans-serif; color:#101010; position:absolute; top:-2px; left:10px;}
#vpp_player_button_area_next {font:500 19px/20px Roboto,arial,sans-serif; color:#101010; position:absolute; top:0px; right:35px;}
.vpp_player_button {position:relative; cursor:pointer; padding:0px 5px; user-select:none; -moz-user-select:none;}
.vpp_player_button[button_kind='plus'], .vpp_player_button[button_kind='minus'] {font:500 20px/20px Roboto,arial,sans-serif !important; top:2px;}
.vpp_player_button[button_kind='left'] {padding:0px 2px 0px 5px;}
.vpp_player_button[button_kind='right'] {padding:0px 2px;}
.vpp_player_button[button_kind='up'] {padding:0px 2px;}
.vpp_player_button[button_kind='down'] {padding:0px 5px 0px 2px;}
.vpp_player_button:hover {color:#E0E0E0;}
#vpp_player_close_mark {font:14px/20px Roboto,arial,sans-serif; position:absolute; top:0px; right:5px; cursor:pointer; user-select:none; -moz-user-select:none;}
#vpp_player_close_mark:hover {color:#E0E0E0;}\
/* float preview */
#vpp_float_box {position:absolute; box-shadow:0px 0px 8px 3px rgba(128,128,128,0.9); background:black; z-index:2147483647;}
#vpp_float_frame {position:relative; width:100%; height:100%; border:0px;}
/* player options */
#vpp_player_options_popup {direction:ltr; position:absolute; left:0px; top:0px; font:11px/11px Roboto,arial,sans-serif; color:white; background:linear-gradient(#888888,#787878); padding:5px; border-radius:5px; /*z-index:2147483647;*/ z-index:2147483646;}
.vpp_player_options_text {font-weight:500; margin-left:5px; margin-top:7px; color:lemonchiffon;}
.vpp_player_options_close {font:14px/14px Roboto,arial,sans-serif; color:black; position:absolute; top:3px; right:5px; cursor:pointer; user-select:none; -moz-user-select:none;}
.vpp_player_options_close:hover {color:lightgray;}
.vpp_player_options_title {font:500 13px/13px Roboto,arial,sans-serif; padding:3px !important; color:lemonchiffon;}
/*other*/
.watched .video-thumb {opacity:1 !important;}
.yt-subscribe-button-right {margin-top:12px !important;}
.pl-video .pl-video-thumbnail,
.pl-video .pl-video-thumb,
.pl-video .yt-thumb {width: 120px !important;}
.pl-video .yt-thumb-clip > img {width:120px !important; height:auto !important;}
.pl-video-time .timestamp {padding-top:18px !important;}
`;


//==============================================================
//basic

const AREA_ID = "vpp_player_area";
const BOX_ID = "vpp_player_box";
const HOLDER_ID = "vpp_player_holder";

function newElem(tag, attrs = {}, text = null, parent = null) {

    const node = document.createElement(tag);
    for (let attr in attrs) {
        node.setAttribute(attr, attrs[attr]);
    }
    if (text) {
        node.textContent = text;
    }

    parent?.appendChild(node);

    return node;
}

function insertStyle(str, id, doc = document) {
    const style = document.getElementById(id);
    if (style) {
        style.textContent = str;
    }
    else {
        newElem("style", {"type": "text/css", "id": id}, str, document.head);
    }
}

function injectScript(str, src, doc = document) {
    if (str) {
        document.head.removeChild(newElem("script", {}, str, document.head));
    }
    else if (src) {
        newElem("script", {"src": src}, null, document.head)
    }
}

function xpath(outer_dom, inner_dom, query) {
    //XPathResult.ORDERED_NODE_SNAPSHOT_TYPE = 7
    return outer_dom.evaluate(query, inner_dom, null, 7, null);
}


function docsearch(query) {
    return xpath(document, document, query);
}


function innersearch(inner, query) {
    return xpath(document, inner, query);
}


function simulClick(el) {
    const clickEvent = document.createEvent("MouseEvents");
    clickEvent.initEvent("click", true, true);
    clickEvent.artificialevent = true;
    el.dispatchEvent(clickEvent);
}

function filter(str, w, delim) {
    if (str) {
        const m = str.match(RegExp(`[${delim}]${w}[^${delim}]*`));
        if (m != null) {
            return m[0].replace(RegExp(`[${delim}]${w}`), "");
        }
    }
    return null;
}

//==============================================================
//preferences

var pref_floatEnable = init_pref("floatEnable", true);
var pref_floatDelay = init_pref("floatDelay", 1);
var pref_playerEnable = init_pref("playerEnable", true);
var pref_rateEnable = init_pref("rateEnable", true);
var pref_defEnable = init_pref("defEnable", true);

function new_plusminus(prefname, str, parent) {
    const div = newElem("div", {"class": "vpp_generic"}, null, parent);
    newElem("span", {"id": "vpp_pref_delay_text"}, str, div);
    const num = newElem("span", {"id": "vpp_pref_delay_num"}, get_pref("floatDelay").toString() + "s", div);
    const minus = newElem("span", {"id": "vpp_pref_delay_minus", "class": "vpp_pref_delay_plusminus"}, "\u2212", div);
    const plus = newElem("span", {"id": "vpp_pref_delay_plus", "class": "vpp_pref_delay_plusminus"}, "\u002B", div);

    minus.onclick = function () {
        let val = get_pref("floatDelay");
        if (val > 0) {
            val--;
            set_pref("floatDelay", val);
            num.textContent = get_pref("floatDelay").toString() + "s";
        }
    }

    plus.onclick = function () {
        let val = get_pref("floatDelay");
        if (val < 5) {
            val++;
            set_pref("floatDelay", val);
            num.textContent = get_pref("floatDelay").toString() + "s";
        }
    }

}


function new_checkbox(prefname, str, kind, parent, value, func = function(){}) {
    const div = newElem(kind, {"class": "vpp_generic"}, null, parent);
    const input = newElem("input", {"class": "vpp_generic", "type": "checkbox"}, null, div);
    if (!value) {
        input.checked = get_pref(prefname);
        input.onclick = function (e) {
            const val = get_pref(prefname);
            set_pref(prefname, !val);
            e.target.checked = !val;
            func();
        };
    }
    else {
        input.value = value;
        input.checked = (get_pref(prefname) == input.value);
        input.onclick = function (e) {
            const val = get_pref(prefname);
            set_pref(prefname, e.target.value);
            e.target.checked = true;
            const other = innersearch(parent.parentNode, `.//input[@value='${val}']`).snapshotItem(0);
            if (other && (other != e.target)) other.checked = false;
            func();
        };
    }
    newElem("span", {"class": "vpp_opendelay"}, str, div);
}


function pref_popup_close() {
    const popup = document.getElementById("vpp_pref_popup");
    popup?.parentNode?.removeChild(popup);
}

function pref_popup_open() {
    if (document.getElementById("vpp_pref_popup")) {
        return;
    }
    const popup = newElem("span", {"id": "vpp_pref_popup"}, null, document.body);
    newElem("div", {"id": "vpp_pref_title"}, "Preview Options", popup);

    let changed = false;

    const closemark = newElem("span", {"id": "vpp_pref_close", "title": "close options"}, "\u2716", popup);
    closemark.onclick = function () {
        popup.parentNode.removeChild(popup);
        if (changed) {
            location.reload();
        }
    };

    let mark = function(){changed = true;};
    new_checkbox("floatEnable", "Hover Preview", "div", popup, null, mark);
    new_plusminus("floatDelay", "open delay", popup);
    new_checkbox("playerEnable", "Click Preview", "div", popup, null, mark);
    new_checkbox("rateEnable", "Video Rating", "div", popup, null, mark);
    new_checkbox("defEnable", "Video Resolution", "div", popup, null, mark);
}


//==================================================================
// Player

const basic_str = "((local-name()='ytd-thumbnail') or (local-name()='ytd-playlist-thumbnail')) and (not(ancestor::*[@hidden]))";
const basic_str2 = "//img[contains(@src,'vi/') or contains(@src,'vi_webp/') or contains(@src,'/p/') or contains(@src,'/s_p/')]";

function player_script() {
    injectScript(`if (YT) {
                            var player = new YT.Player('vpp_player_frame');
                            var errort = null;
                            function error_reset() { if (errort) clearTimeout(errort); }
                            function check_error(t,fid) {
                               error_reset();
                               errort = setTimeout(
                                    function (fid) {
                                       var f = document.getElementById('vpp_player_frame');
                                       if (!f) return;
                                       if (f.getAttribute('fid') != fid) return;
									   fdoc = f.contentWindow.document;
                                       var s = player.getPlayerState();
                                       var a = player.getPlaylist();
                                       var i = player.getPlaylistIndex();
                                       if (a != null ? a.length == 0 : false)
                                           f.dispatchEvent(new Event('playend'));
                                       else
                                           if ((s == -1 && fdoc.getElementsByClassName('ytp-error').length > 0) || s == 5)
                                              if ((a != null && i != null) ? i < a.length - 1 : false)
                                                 player.nextVideo();
                                              else
                                                 f.dispatchEvent(new Event('playend'));
                                    }, t, fid);
                            }
                            player.addEventListener('onReady',
                                   function () {
                                        var f = document.getElementById('vpp_player_frame');
                                        if (f) {
                                             var q = f.getAttribute('quality');
                                             if (q != 'default' && q != null) player.setPlaybackQualityRange(q);
                                         }
                                   });
                            player.addEventListener('onStateChange',
                                 function () {
                                    var f = document.getElementById('vpp_player_frame');
                                    if (!f) return;
                                    var fid = f.getAttribute('fid');
                                    var q = f.getAttribute('quality');
                                    var s = player.getPlayerState();
                                    var a = player.getPlaylist();
                                    var i = player.getPlaylistIndex();
                                    var cond = ((a != null && i != null) ? i == a.length - 1 : true);
                                    if (s == -1 || s == 5) check_error(10000,fid); else error_reset();
                                    if (s == -1 && q != 'default' && q != null) player.setPlaybackQualityRange(q);
                                    if (s == 0 && cond) f.dispatchEvent(new Event('playend'));
                                 });
                            var frame = document.getElementById('vpp_player_frame');
                            if (frame) {
                                  check_error(10000,frame.getAttribute('fid'));
                                  frame.addEventListener('loadnewvideo',
                                                          function (x) {
                                                             var fid = x.target.getAttribute('fid');
                                                             var url = x.target.getAttribute('newvidurl');
                                                             var plist = x.target.getAttribute('plist');
                                                             player.pauseVideo();
                                                             if (plist) {
                                                                player.loadPlaylist({'list':plist});
                                                                check_error(10000,fid);
                                                             }
                                                             else if (url) {
                                                                player.loadVideoByUrl(url);
                                                                check_error(10000,fid);
                                                             }
                                                          });
                            }
                     }`);
}

const choices_def = ['default', 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'highres'];
const choices_size = ['xxsmall', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'xxlarge'];
const choices_pos = ['00', '01', '02', '10', '11', '12', '20', '21', '22'];

const scriptPrefs = new Proxy(JSON.parse(GM_getValue("YTVPR_prefs",
     `{
        "floatEnable": true,
        "floatDelay": 1,
        "playerEnable": true,
        "rateEnable": true,
        "defEnable": true,
        "playerFit": false,
        "playerDef": 0,
        "playerSize": 3,
        "playerPosLeft": 1,
        "playerPosTop": 1,
        "playerNext": true,
        "playerClose": true,
        "playerPause": true,
        "playerDim": true
     }`)),
     {
    set: function(obj, prop, value) {
        const clamp = function(val, min, max){ return Math.min(Math.max(min, val), max); };
        switch (prop) {
            case "playerDef": value = clamp(value, 0, choices_def.length()-1); break;
            case "playerSize": value = clamp(value, 0, choices_size.length()-1); break;
            case "playerPosLeft":
            case "playerPosTop": value = clamp(value, 0, 2); break;
        }

        if(obj[prop] != value) {
            obj[prop] = value;
            GM_setValue("YTVPR_prefs", JSON.stringify(obj));
        }
        return true;
    }
});

//player preferences
if (pref_playerEnable) {
    init_pref("playerFit", false);
    init_pref("playerDef", "default");
    init_pref("playerSize", "medium");
    init_pref("playerPos", "11");
    init_pref("playerNext", true);
    init_pref("playerClose", true);
    init_pref("playerPause", true);
    init_pref("playerDim", true);

    //fix char preferences
    if (choices_def.indexOf(get_pref("playerDef")) < 0) set_pref("playerDef", 'default');
    if (choices_size.indexOf(get_pref("playerSize")) < 0) set_pref("playerSize", 'medium');
    if (choices_pos.indexOf(get_pref("playerPos")) < 0) set_pref("playerPos", '11');
}


function player_options(parent) {
    if (document.getElementById("vpp_player_options_popup")) {
        return;
    }

    const popup = newElem("span", {"id": "vpp_player_options_popup"}, null, parent);
    newElem("div", {"class": "vpp_player_options_title"}, "Player Options", popup);

    const closemark = newElem("span", {"class": "vpp_player_options_close", "title": "close options"}, "\u2716", popup);
    closemark.onclick = close_player_options;

    new_checkbox("playerNext", "Auto Play Next", "div", popup);
    new_checkbox("playerDim", "Dim Background", "div", popup, null, function () {
        document.getElementById(AREA_ID).style.visibility = (get_pref("playerDim") ? "visible" : "hidden");
    });
    new_checkbox("playerClose", "Close by Clicking Outside Player", "div", popup);
    new_checkbox("playerPause", "Pause YouTube Player at Launch", "div", popup);

    newElem("div", {"class": "vpp_player_options_text"}, "Resolution", popup);
    //default, small, medium, large, hd720, hd1080, hd1440, highres;
    const group1 = newElem("div", {"class": "vpp_player_options_group"}, null, popup);
    const group2 = newElem("div", {"class": "vpp_player_options_group"}, null, popup);
    new_checkbox("playerDef", "Default", "span", group1, "default");
    new_checkbox("playerDef", "LQ 240", "span", group1, "small");
    new_checkbox("playerDef", "MQ 360", "span", group1, "medium");
    new_checkbox("playerDef", "HQ 480", "span", group1, "large");
    new_checkbox("playerDef", "HD 720", "span", group2, "hd720");
    new_checkbox("playerDef", "HD 1080", "span", group2, "hd1080");
    new_checkbox("playerDef", "HD 1440", "span", group2, "hd1440");
    new_checkbox("playerDef", "MAX", "span", group2, "highres");
}

function close_player_options() {
    const popup = document.getElementById("vpp_player_options_popup");
    if (popup) {
        popup.parentNode.removeChild(popup);
    }
}


function playerUrl(vid, pid) {
    let url = `${location.protocol}//${location.hostname}/`;

    if (vid) {
        url = `${url}embed/${vid}?`;
        if (pid) {
            url = `${url}list=${pid}`;
        }
    }
    else if (pid) {
        url = `${url}embed?listType=playlist&list=${pid}`;
    }
    else {
        console.error("Null pid and vid!");
    }

    url = `${url}&autoplay=1&fs=1&iv_load_policy=3&rel=1&version=3&enablejsapi=1`;

    return (url);
}


function adjust_playing(node) {
    const playing = document.getElementById("vpp_now_playing");
    playing?.parentNode?.removeChild(playing);
    if (node) {
        newElem("span", {"id": "vpp_now_playing"}, "now playing", node);
    }
}

function build_player() {
    //constants
    var next_choice = new Object();
    next_choice['plus'] = new Object();
    next_choice['minus'] = new Object();
    next_choice['left'] = new Object();
    next_choice['right'] = new Object();
    next_choice['up'] = new Object();
    next_choice['down'] = new Object();

    next_choice['plus']['xxsmall'] = 'xsmall';
    next_choice['plus']['xsmall'] = 'small';
    next_choice['plus']['small'] = 'medium';
    next_choice['plus']['medium'] = 'large';
    next_choice['plus']['large'] = 'xlarge';
    next_choice['plus']['xlarge'] = 'xxlarge';
    next_choice['plus']['xxlarge'] = 'xxlarge';

    next_choice['minus']['xxsmall'] = 'xxsmall';
    next_choice['minus']['xsmall'] = 'xxsmall';
    next_choice['minus']['small'] = 'xsmall';
    next_choice['minus']['medium'] = 'small';
    next_choice['minus']['large'] = 'medium';
    next_choice['minus']['xlarge'] = 'large';
    next_choice['minus']['xxlarge'] = 'xlarge';

    {
        for (var i = 0; i < 3; i++)
            for (var j = 0; j < 3; j++) {
                next_choice['left'][i.toString() + j.toString()] = i.toString() + (j - 1 >= 0 ? j - 1 : 0).toString();
                next_choice['right'][i.toString() + j.toString()] = i.toString() + (j + 1 <= 2 ? j + 1 : 2).toString();
                next_choice['up'][i.toString() + j.toString()] = (i - 1 >= 0 ? i - 1 : 0).toString() + j.toString();
                next_choice['down'][i.toString() + j.toString()] = (i + 1 <= 2 ? i + 1 : 2).toString() + j.toString();
            }
    }

    var new_size = get_pref("playerSize");
    var new_pos = get_pref("playerPos");
    var new_fit = get_pref("playerFit");

    var that = this;
    var frame_count = 0;

    //public
    this.playerShow = function (vid, pid, node) {
        const box = document.getElementById(BOX_ID);
        if (box?.style.visibility == "hidden") {
            new_size = get_pref("playerSize");
            new_pos = get_pref("playerPos");
            new_fit = get_pref("playerFit");
        }
        playerAdjust((new_fit ? "fit" : new_size), new_pos, "visible", vid, pid);
        adjust_playing(node);
    }

    this.playerClose = function () {
        playerAdjust(null, null, "hidden", null, null);
        close_player_options();
        adjust_playing();
    }


    //private
    function play_next(findprevious) {
        const playing = document.getElementById("vpp_now_playing");
        if (!playing) {
            return;
        }

        const myimg = innersearch(playing.parentNode, ".//img[@src or @data-thumb]").snapshotItem(0);
        if (myimg) {
            let pos = -2;
            let l = docsearch(`//*[(${basic_str})]${basic_str2}`);

            myimg.setAttribute("matchfind", "true");

            for (let i = 0; i < l.snapshotLength; i++) {
                if (l.snapshotItem(i).getAttribute("matchfind")) {
                    pos = i;
                    break;
                }
            }

            myimg.removeAttribute("matchfind");
            pos = (findprevious ? pos - 1 : pos + 1);

            if (pos >= 0) {
                const img = l.snapshotItem(pos);
                if (img) {
                    img.setAttribute("matchfindimg", true);
                    const target = docsearch(`//*[${basic_str} and (.//img[@matchfindimg])]`).snapshotItem(0);
                    if (target) {
                        that.playerShow(find_vid(img), find_plist(img), target);
                    }
                    img.removeAttribute("matchfindimg");
                }
            }
        }
    }

    function playerAdjust(size, pos, vis, vid, pid) {
        const box = document.getElementById(BOX_ID);
        if (vis != null) {
            box.style.visibility = vis;
            area.style.visibility = (get_pref("playerDim") ? vis : "hidden");

            let frame = document.getElementById("vpp_player_frame");
            if (frame && (vis == "hidden")) {
                frame.parentNode.removeChild(frame);
                frame = null;
            }

            if (vis == "visible") {
                const vidurl = playerUrl(vid, pid);
                const def = get_pref("playerDef");
                frame_count++;

                if (frame) {
                    frame.setAttribute("newvidurl", vidurl);
                    if (pid) frame.setAttribute("plist", pid);
                    else frame.removeAttribute("plist");
                    frame.setAttribute("quality", def);
                    frame.setAttribute("fid", frame_count.toString());

                    const event = document.createEvent("Event");
                    event.initEvent("loadnewvideo", true, true);
                    frame.dispatchEvent(event);
                }
                else {
                    frame = newElem("iframe", {"id": "vpp_player_frame", "type": "text/html", "frameborder": "0", "allowfullscreen": "true", "quality": def, "fid": frame_count.toString(), "src": vidurl}, null, document.getElementById(HOLDER_ID).firstElementChild);
                    frame.addEventListener("playend", function(){if (get_pref("playerNext")) play_next();});
                    player_script();
                }
            }
        }

        if (size != null) {
            box.setAttribute("player_size", size);
            holder.setAttribute("player_size", size);
        }

        if (pos != null) {
            box.setAttribute("player_pos", pos);
        }
    }

    function click_pos_size(e) {
        var kind = e.target.getAttribute("button_kind");
        if (!kind) return;

        if (new_fit && !(kind == "options" || kind == "prev" || kind == "next")) {
            set_pref("playerFit", false);
            new_fit = false;
            playerAdjust(new_size, new_pos);
            return;
        }

        switch (kind) {
            case 'plus':
            case 'minus':
                new_size = next_choice[kind][new_size];
                set_pref("playerSize", new_size);
                playerAdjust(new_size, new_pos);
                break;

            case 'fit':
                set_pref("playerFit", true);
                new_fit = true;
                playerAdjust('fit');
                break;

            case 'left':
            case 'right':
            case 'up':
            case 'down':
                new_pos = next_choice[kind][new_pos];
                set_pref("playerPos", new_pos);
                playerAdjust(new_size, new_pos);
                break;

            case 'options':
                player_options(document.getElementById(BOX_ID));
                break;

            case 'prev':
                play_next(true);
                break;

            case 'next':
                play_next();
                break;
        }
    }

    function new_button(kind, str, str_popup, parent) {
        newElem("span", {"class": "vpp_player_button", "title": str_popup, "button_kind": kind}, str, parent).onclick = click_pos_size;
    }

    //initialization;
    if (document.getElementById(AREA_ID)) {
        return;
    }

    const area = newElem("div", {"id": AREA_ID, "style": "visibility: hidden;"}, null, document.body);
    const area2 = newElem("div", {"id": "vpp_player_area2"}, null, area);
    const box = newElem("div", {"id": BOX_ID, "style": "visibility: hidden;"}, null, area2);
    const holder = newElem("div", {"id": HOLDER_ID}, null, box);
    newElem("div", {"id": "vpp_player_holder2"}, null, holder);
    area.onclick = function (e) { if (e.target.id == AREA_ID && get_pref("playerClose")) that.playerClose(); };

    const buttonArea = newElem("span", {"id": "vpp_player_button_area_top"}, null, box);
    new_button("plus", "\u002B", "increase size", buttonArea);
    new_button("minus", "\u2212", "decrease size", buttonArea);
    new_button("fit", "\u2610", "fill window", buttonArea);
    new_button('left', '\u25C4', 'move left', buttonArea);
    new_button('right', '\u25BA', 'move right', buttonArea);
    new_button('up', '\u25B2', 'move up', buttonArea);
    new_button('down', '\u25BC', 'move down', buttonArea);
    new_button("options", "\u2630", "player options", buttonArea);

    const bottomArea = newElem("span", {"id": "vpp_player_button_area_next"}, null, box);
    new_button("prev", "\u140A\u140A", "play previous in page", bottomArea);
    new_button("next", "\u1405\u1405", "play next in page", bottomArea);
    //new_button("loop", "\u21BB", "repeat video", bottomArea);

    newElem("span", {"id": "vpp_player_close_mark", "title": "close player"}, "\u2716", box).onclick = this.playerClose;
}

var player = null;
if (pref_playerEnable) {
    player = new build_player();
    injectScript(null, "https://www.youtube.com/iframe_api");
}

function ytpause() {
    const func = `(function(){
        const mainVid = document.getElementById("movie_player");
        const channelVid = document.getElementById("c4-player");
        if (mainVid && (mainVid.getPlayerState() == 1))
            mainVid.pauseVideo();
        if (channelVid && (channelVid.getPlayerState() == 1))
            channelVid.pauseVideo();
    })();`;
    injectScript(func);
}


//==================================================================
// float player

//quality is default for faster upload, and youtube player is paused
function float_script() {
    const func = `(function(){
        const fplayer = new YT.Player("vpp_float_frame");
        fplayer.addEventListener("onReady", function () {
            const mainVid = document.getElementById("movie_player");
            const channelVid = document.getElementById("c4-player");
            if (mainVid && (mainVid.getPlayerState() == 1))
                mainVid.pauseVideo();
            if (channelVid && (channelVid.getPlayerState() == 1))
                channelVid.pauseVideo();
            fplayer.setPlaybackQualityRange("default");
            });
    })();`;
    injectScript(func);
}

function float_open(e, check) {
    //check tests if same url frame is already open
    const v_id = e.target.firstElementChild?.href?.match(/(?<=v=)[a-zA-Z0-9_-]*/)?.[0];

    if (!v_id || document.getElementById("vpp_player_frame")) {
        return false;
    }

    if (!check) {
        float_delay_clear();
        adjust_playing(e.target);
    }

    const url = `https://www.youtube.com/embed/${v_id}?&autoplay=1&controls=1&iv_load_policy=3&rel=0&showinfo=1&version=3&enablejsapi=1`;
    const frame = document.getElementById("vpp_float_frame");
    if (frame) {
        if (frame.src == url) {
            return check;
        }
        if (!check) {
            frame.parentNode.removeChild(frame);
        }
    }

    if (check) {
        return false;
    }

    const float_width = 480; //512;
    const float_height = 270; //288;
    let box = document.getElementById("vpp_float_box");
    if (!box) {
        box = newElem("div", {"id": "vpp_float_box", "style": `width: ${float_width}px; height: ${float_height}px;`}, null, document.body);
        box.onmouseenter = float_delay_clear;
        box.onmouseleave = float_close_delay;
    }

    const r = e.target.getBoundingClientRect();
    const w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
    const h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
    const hpad = Math.round(-r.width / 3); //horizontal offset
    const vpad = 0; //vertical offset

    //priority to right
    let left = r.right + hpad;
    if ((left + float_width > w) && ((r.left - float_width - hpad >= 0) || (r.left > w - r.right))) {
            left = r.left - float_width - hpad;
    }

    //priority to left
    //let left = r.left - float_width - pad;
    //if (left < 0)
    //    if ((r.right + float_width + pad <= w) || (w - r.right > r.left))
    //        left = r.right + pad;

    //priority to top
    let top = r.top - float_height - vpad;
    if (top < 0 && (r.bottom + float_height + vpad <= h || h - r.bottom > r.top)) {
        top = r.bottom + vpad;
    }

    left += (document.body.scrollLeft || document.documentElement.scrollLeft);
    top += (document.body.scrollTop || document.documentElement.scrollTop);

    box.style.left = left + "px";
    box.style.top = top + "px";

    newElem("iframe", {"id": "vpp_float_frame", "type": "text/html", "frameborder": "0", "src": url}, null, box);

    float_script();
    return true;
}

let float_open_timeout = null;
let float_close_timeout = null;

function float_close(e) {
    clearTimeout(float_close_timeout);
    const box = document.getElementById("vpp_float_box");

    //check if mouse was in the area
    if (box) {
        if (!e || (e.target != box)) {
            box.parentNode.removeChild(box);
            adjust_playing();
        }
        else { //player should not close if mouse is still inside
            const r = box.getBoundingClientRect();
            if ((e.clientX <= r.left + 1) || (e.clientX >= r.right - 1) ||
                (e.clientY <= r.top + 1) || (e.clientY >= r.bottom - 1)) {
                box.parentNode.removeChild(box);
                adjust_playing();
            }
        }
    }
}


function float_delay_clear() {
    clearTimeout(float_open_timeout);
    clearTimeout(float_close_timeout);
}

function float_reset() {
    float_delay_clear();
    float_close();
}

function float_open_delay(e) {
    if (float_open(e, true)) {
        float_delay_clear();
    }
    else {
        clearTimeout(float_open_timeout);
        const delay = get_pref("floatDelay") * 1000;
        float_open_timeout = setTimeout(float_open.bind(null, e, false), delay);
    }
}

function float_close_delay(e) {
    float_delay_clear();
    float_close_timeout = setTimeout(float_close.bind(null, e), 200);
}


//==================================================================
//meta data

function innertube_callback(data, parent) {
    let max_res = 0;
    let max_quality_label = "";
    let formats = [];

    if (data.streamingData?.hasOwnProperty("formats")) {
        formats = formats.concat(data.streamingData.formats);
    }
    if (data.streamingData?.hasOwnProperty("adaptiveFormats")) {
        formats = formats.concat(data.streamingData.adaptiveFormats);
    }
    for (let i = 0; i < formats.length; i++) {
        const res = formats[i].width * formats[i].height;
        if (res > max_res) {
            max_res = res;
            max_quality_label = formats[i].qualityLabel;
        }
    }
    if(max_res != 0) {
        parent.setAttribute("vpp_meta_def", "");
        const def_node = newElem("span", {"class": "vpp_meta_def_container", "reveal": "true", "title": max_quality_label}, null, parent);
        const def_txt =
            max_res >= (15360 * 8640) ? "16K" :
            max_res >= (7680 * 4320) ? "8K" :
            max_res >= (2880 * 2160) ? "4K" :
            max_res >= (960 * 1080) ? "1080p" :
            max_res >= (640 * 720) ? "720p" : "SD";
        newElem("span", {"class": "vpp_meta_def_hd HD"}, def_txt, def_node);
    }
}

function rytdl_callback(data, parent) {
        if (data.likes > 0 || data.dislikes > 0) {
            const perc = Math.round((data.rating/5)*100);
            // Color scale from 50 (red) to 100 (green)
            const scaled = Math.max(0,(perc-50)/50);
            const r = Math.min(0xC0,Math.round(2*0xC0*(1-scaled)));
            const g = Math.min(0xA0,Math.round(2*0xA0*(scaled)));
            const b = 0;
            const rgb = (r << 16) | (g << 8) | b;
            const hex = `#${rgb.toString(16).padStart(6, "0")}`;

            newElem("div",
                    { "class": "vpp_meta_rate",
                      "style": `background:${hex} !important;`,
                      "title": `${Number(data.viewCount).toLocaleString()} views`
                    },
                    `${perc}`,
                    parent);
            parent.setAttribute("vpp_meta_rate", "");
        }
}

var innertube_key, client_version;
const scripts = document.getElementsByTagName("script");
for (let script of scripts) {
    innertube_key = script.textContent?.match(/"INNERTUBE_API_KEY":"([^"]*?)"/)?.[1];
    if (innertube_key) {
        client_version = script.textContent?.match(/"INNERTUBE_CLIENT_VERSION":"([^"]*?)"/)?.[1];
        break;
    }
}

const rytdl_queue = new Map();
const rytdl_counter = [];
function rytdl_req() {
    const v_id = rytdl_queue.keys().next().value;
    const rytdlReq = new XMLHttpRequest();
    rytdlReq.responseType = "json";
    rytdlReq.addEventListener("loadend", function() {
        if (this.response) {
            rytdl_callback(this.response, rytdl_queue.get(v_id));
        } else {
            console.error("rytdl request failed");
        }
        rytdl_queue.delete(v_id);
        if (rytdl_queue.size > 0) {
            let timeout = 600; // 100 reqs/min
            /* Needs Timing-Allow-Origin from server for this to work
            if (this.response) {
                const resources = performance.getEntriesByType("resource");
                for (let entry of resources) {
                    if (entry.name === this.responseURL && entry.transferSize === 0) {
                        timeout = 0;
                        break;
                    }
                }
            } */
            setTimeout(rytdl_req, timeout);
        }
    });
    rytdlReq.open("GET", `https://returnyoutubedislikeapi.com/Votes?videoId=${v_id}`);
    rytdlReq.send();
}

function def_rate(v_id, parent) {
    debug(`Getting info for ${v_id}`);
    if (pref_defEnable) {
        const params = {
            "context": {
                "client": {
                    "hl": "en",
                    "clientName": "WEB",
                    "clientVersion": `${client_version}`,
                    "clientFormFactor": "UNKNOWN_FORM_FACTOR",
                    "clientScreen": "WATCH",
                    "mainAppWebInfo": {
                        "graftUrl": `/watch?v=${v_id}`,
                    }
                },
                "user": {
                    "lockedSafetyMode": false
                },
                "request": {
                    "useSsl": true,
                    "internalExperimentFlags": [],
                    "consistencyTokenJars": []
                }
            },
            "videoId": `${v_id}`,
            "playbackContext": {
                "contentPlaybackContext": {
                    "vis": 0,
                    "splay": false,
                    "autoCaptionsDefaultOn": false,
                    "autonavState": "STATE_NONE",
                    "html5Preference": "HTML5_PREF_WANTS",
                    "lactMilliseconds": "-1"
                }
            },
            "racyCheckOk": false,
            "contentCheckOk": false
        };

        const innertubeReq = new XMLHttpRequest();
        innertubeReq.responseType = "json";
        innertubeReq.addEventListener("loadend", function(){
            if (this.response) {
                innertube_callback(this.response, parent);
            } else {
                console.error("innertube request failed");
            }
        });
        // https://stackoverflow.com/questions/67615278/get-video-info-youtube-endpoint-suddenly-returning-404-not-found
        innertubeReq.open("POST", `https://www.youtube.com/youtubei/v1/player?key=${innertube_key}`);
        innertubeReq.send(JSON.stringify(params));
    }

    if (pref_rateEnable) {
        rytdl_queue.set(v_id, parent);
        if (rytdl_queue.size === 1) {
            rytdl_req();
        }
    }
}


function find_vid(img) {
    return (filter(img.src, "vi/", "/&?#") || filter(img.src, "vi_webp/", "/&?#"));
}


function find_plist(img) {
    let plist = null;
    const anc = innersearch(img, "ancestor-or-self::*[contains(@href,'&list=') and (.//*[contains(@class,'yt-pl-sidebar-content') or contains(@class,'ytd-thumbnail-overlay-side-panel-renderer')])]").snapshotItem(0);
    if (anc) {
        plist = filter(anc.href, "list=", "/&?#");
        if (plist == "WL") {
            plist = null;
        }
    }
    return plist;
}


function play(parent) {
    const playArea = newElem("div", {"class": "vpp_play_button_container"}, null, parent);
    const playNode = newElem("a", {"class": "vpp_play_button", "href": "javascript:;", "target": "_self", "title": "click to preview"}, null, playArea);

    const play_handle = function(e) {
        e.stopPropagation();
        float_reset();

        const parpar = e.target.parentNode.parentNode;
        if (innersearch(parpar, ".//*[@id='vpp_now_playing']").snapshotLength > 0) {
            player.playerClose();
        }
        else {
            const img = innersearch(parpar, `.${basic_str2}`).snapshotItem(0);
            if (img) {
                player.playerShow(find_vid(img), find_plist(img), parpar);
                if (get_pref("playerPause")) {
                    ytpause();
                }
            }
            else {
                console.error("play(parent): img not found");
            }
        }
    }

    playNode.onclick = play_handle;
}


//==================================================================
// Main
debug("***YouTube Video Preview and Ratings Keyless***");

//insert styles
insertStyle(style_basic, "vpp_style_basic");

if (pref_playerEnable) {//hide overlay of playlist
    insertStyle(".yt-pl-thumb-overlay, ytd-thumbnail-overlay-hover-text-renderer {display:none !important;}", "vpp_style_list_overlay");//OLD,NEW
}

(function insertMenuBtn() {
    const par = document.getElementById("masthead-container");
    if (par) {
        newElem("span", {"id": "vpp_pref_button", "title": "Preview Options"}, null, par).onclick = pref_popup_open;
    }
    else {
        new MutationObserver(function(mutations, observer) {
            observer.disconnect();
            insertMenuBtn();
        }).observe(document.getElementById("masthead"), {childList: true});
    }
})();

function processThumbs(thumbs) {
    for (let thumb of thumbs) {
        const parent = thumb.parentNode;
        const v_id = thumb.href?.match(/(?<=v=|shorts\/)[a-zA-Z0-9_-]*/)?.[0];
        if (v_id) {
            if (pref_defEnable || pref_rateEnable) {
                def_rate(v_id, parent);
            }
            if (pref_floatEnable) {
                parent.onmouseenter = float_open_delay;
                parent.onmouseleave = float_close_delay;
            }
            if (pref_playerEnable) {
                play(parent);
            }
        }
    }
}
processThumbs(Array.from(document.body.querySelectorAll("#thumbnail")));

new MutationObserver(function(mutations, observer) {
    for (let mutation of mutations) {
        const thumbs = Array.from(mutation.addedNodes).filter(node => (node.nodeType === Node.ELEMENT_NODE && node.id === "thumbnail"));
        processThumbs(thumbs);
    }
}).observe(document.body, {childList: true, subtree: true});