YouTube Enhanced Player

Remembers video playback position and volume, resumes from that point (minus 5 seconds)

// ==UserScript==
// @name         YouTube Enhanced Player
// @name:en      YouTube Enhanced Player
// @name:es      YouTube Reproductor Mejorado
// @namespace    http://tampermonkey.net/
// @version      1.6.3.1
// @description  Запоминает позицию просмотра видео и громкость, возобновляет с этого места (минус 5 секунд)
// @description:en Remembers video playback position and volume, resumes from that point (minus 5 seconds)
// @description:es Recuerda la posición y volumen de reproducción, continúa desde ese punto (menos 5 segundos)
// @author       LegonYY
// @match        https://www.youtube.com/*
// @grant        none
// @icon         https://img.icons8.com/?size=100&id=55200&format=png&color=000000
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    function getVideoId() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('v');
    }

    function saveVideoTime(videoId, currentTime) {
        localStorage.setItem(`yt_time_${videoId}`, currentTime.toString());
    }

    function loadVideoTime(videoId) {
        const savedTime = localStorage.getItem(`yt_time_${videoId}`);
        return savedTime ? parseFloat(savedTime) : 0;
    }

    function saveVolumeLevel(volume) {
        localStorage.setItem('yt_volume_global', volume.toString());
    }

    function loadVolumeLevel() {
        const savedVolume = localStorage.getItem('yt_volume_global');
        return savedVolume ? parseFloat(savedVolume) : 100;
    }

    function showSaveNotification() {
        const overlay = document.querySelector('.html5-video-player .ytp-player-content')
            || document.querySelector('.ytp-chrome-top')
            || document.body;

        if (getComputedStyle(overlay).position === 'static') {
            overlay.style.position = 'relative';
        }

        const old = overlay.querySelector('.timeSaveNotification');
        if (old) old.remove();

        const notif = document.createElement('div');
        notif.className = 'timeSaveNotification';
        Object.assign(notif.style, {
            position: 'absolute',
            bottom: '0px',
            right: '5px',
            background: 'rgba(0,0,0,0.7)',
            color: '#fff',
            padding: '5px 10px',
            borderRadius: '5px',
            zIndex: '9999',
            fontSize: '14px',
            opacity: '0',
            transition: 'opacity 0.5s ease',
        });
        notif.innerText = 'Время просмотра сохранено!';
        overlay.appendChild(notif);
        requestAnimationFrame(() => notif.style.opacity = '1');
        setTimeout(() => {
            notif.style.opacity = '0';
            setTimeout(() => notif.remove(), 500);
        }, 3000);
    }

function initResumePlayback() {
    const video = document.querySelector('video');
    if (!video) return;

    const videoId = getVideoId();
    if (!videoId) return;

    const savedTime = loadVideoTime(videoId);


    video.addEventListener('loadedmetadata', () => {
        if (savedTime > 0 && video.duration > savedTime - 5) {
            const resumeTime = Math.max(0, savedTime - 5);
            video.currentTime = resumeTime;
        }
    });

    setInterval(() => {
        if (!video.paused && !video.seeking) {
            const videoId = getVideoId();
            if (videoId) {
                saveVideoTime(videoId, video.currentTime);
            }
        }
    }, 5000);

    window.addEventListener('beforeunload', () => {
        const videoId = getVideoId();
        if (videoId) {
            saveVideoTime(videoId, video.currentTime);
        }
    });
}


    function calculateVolume(position, sliderMax) {
        const volume = (position / sliderMax) * 1400;
        return volume.toFixed();
    }

    function updateVolumeDisplay(volume) {
        const old = document.getElementById('customVolumeDisplay');
        if (old) old.remove();

        const btn = document.getElementById('volumeBoostButton');
        if (!btn) return;

        const volumeDisplay = document.createElement('div');
        volumeDisplay.id = 'customVolumeDisplay';
        volumeDisplay.innerText = `${volume}%`;

        Object.assign(volumeDisplay.style, {
            position: 'absolute',
            fontSize: '14px',
            background: 'rgba(0,0,0,0.8)',
            color: '#fff',
            borderRadius: '5px',
            whiteSpace: 'nowrap',
            padding: '2px 6px',
            pointerEvents: 'none',
            transition: 'opacity 0.3s ease, transform 0.3s ease',
            opacity: '0',
            transform: 'translate(-50%, -10px)',
        });

        const btnContainer = btn.parentElement;
        btnContainer.style.position = 'relative';
        btnContainer.appendChild(volumeDisplay);

        const btnRect = btn.getBoundingClientRect();
        const containerRect = btnContainer.getBoundingClientRect();
        const offsetX = btnRect.left - containerRect.left + btnRect.width / 2;
        const offsetY = btnRect.top - containerRect.top;

        volumeDisplay.style.left = `${offsetX}px`;
        volumeDisplay.style.top = `${offsetY}px`;

        requestAnimationFrame(() => {
            volumeDisplay.style.opacity = '1';
            volumeDisplay.style.transform = 'translate(-50%, -20px)';
        });

        setTimeout(() => {
            volumeDisplay.style.opacity = '0';
            volumeDisplay.style.transform = 'translate(-50%, -10px)';
            setTimeout(() => volumeDisplay.remove(), 300);
        }, 1000);
    }

    function createControlPanel(video) {
        const style = document.createElement('style');
        style.textContent = `
        #volumeBoostButton input[type=range] {
            -webkit-appearance: none;
            width: 100px;
            height: 4px;
            background: #ccc;
            border-radius: 2px;
            outline: none;
        }
        #volumeBoostButton input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: #fff;
            cursor: pointer;
            box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
        }`;
        document.head.appendChild(style);

        const saveButton = document.createElement('button');
        saveButton.id = 'manualSaveButton';
        saveButton.innerText = '💾';
        Object.assign(saveButton.style, {
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            color: '#fff',
            fontWeight: 'bold',
            marginRight: '1px',
            fontSize: '18px',
            transition: 'transform 0.2s ease',
        });
        saveButton.title = 'Сохранить текущее время просмотра';

        const volumeBoostButton = document.createElement('button');
        volumeBoostButton.id = 'volumeBoostButton';
        volumeBoostButton.innerText = '🔊';
        Object.assign(volumeBoostButton.style, {
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            color: '#fff',
            fontWeight: 'bold',
            marginRight: '1px',
            fontSize: '18px',
            transition: 'transform 0.2s ease',
        });
        volumeBoostButton.title = 'Усилитель громкости';

        const customVolumeSlider = document.createElement('input');
        Object.assign(customVolumeSlider, {
            type: 'range',
            min: '100',
            max: '1400',
            step: '1',
        });

        Object.assign(customVolumeSlider.style, {
            display: 'none',
            opacity: '0',
            transform: 'scale(0.8)',
            transition: 'opacity 0.3s ease, transform 0.3s ease',
        });

        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const gainNode = audioContext.createGain();
        gainNode.connect(audioContext.destination);
        const videoSource = audioContext.createMediaElementSource(video);
        videoSource.connect(gainNode);

        const initialVolume = loadVolumeLevel();
        gainNode.gain.value = initialVolume / 100;
        customVolumeSlider.value = initialVolume.toString();
        updateVolumeDisplay(initialVolume.toString());

        customVolumeSlider.addEventListener('input', function () {
            const volume = calculateVolume(this.value, this.max);
            gainNode.gain.value = volume / 100;
            updateVolumeDisplay(volume);
            saveVolumeLevel(volume);
        });

        function resetVolumeTo100() {
            customVolumeSlider.value = '100';
            gainNode.gain.value = 1.0;
            updateVolumeDisplay('100');
            saveVolumeLevel(100);
        }

        volumeBoostButton.addEventListener('mouseenter', () => {
            customVolumeSlider.style.display = 'block';
            requestAnimationFrame(() => {
                customVolumeSlider.style.opacity = '1';
                customVolumeSlider.style.transform = 'scale(1)';
            });
        });

        volumeBoostButton.addEventListener('click', (e) => {
            e.stopPropagation();
            resetVolumeTo100();
        });

        let hideTimeout;
        const sliderContainer = document.createElement('div');
        sliderContainer.style.display = 'flex';
        sliderContainer.style.alignItems = 'center';
        sliderContainer.style.position = 'relative';
        sliderContainer.style.cursor = 'pointer';

        sliderContainer.addEventListener('mouseleave', () => {
            hideTimeout = setTimeout(() => {
                customVolumeSlider.style.opacity = '0';
                customVolumeSlider.style.transform = 'scale(0.8)';
                setTimeout(() => {
                    customVolumeSlider.style.display = 'none';
                }, 300);
            }, 300);
        });

        sliderContainer.addEventListener('mouseenter', () => {
            clearTimeout(hideTimeout);
        });

        const controls = document.querySelector('.ytp-chrome-controls');
        if (controls) {
            const buttonContainer = document.createElement('div');
            buttonContainer.style.display = 'flex';
            buttonContainer.style.alignItems = 'center';
            buttonContainer.style.marginRight = '10px';

            sliderContainer.appendChild(volumeBoostButton);
            sliderContainer.appendChild(customVolumeSlider);
            buttonContainer.appendChild(saveButton);
            buttonContainer.appendChild(sliderContainer);
            controls.insertBefore(buttonContainer, controls.firstChild);

            sliderContainer.addEventListener('wheel', (e) => {
                e.preventDefault();
                const step = 50;
                let val = parseInt(customVolumeSlider.value, 10);
                if (e.deltaY < 0) {
                    val = Math.min(val + step, parseInt(customVolumeSlider.max, 10));
                } else {
                    val = Math.max(val - step, parseInt(customVolumeSlider.min, 10));
                }
                customVolumeSlider.value = val;
                customVolumeSlider.dispatchEvent(new Event('input'));
            });
        }

        saveButton.addEventListener('click', () => {
            const videoId = getVideoId();
            if (videoId) {
                saveVideoTime(videoId, video.currentTime);
                showSaveNotification();
            }
        });
    }

function init() {
    initResumePlayback();
    const video = document.querySelector('video');
    if (video) {
        createControlPanel(video);
        createSpeedControl();
    }
}


    const checkVideo = setInterval(() => {
        if (document.querySelector('video') && document.querySelector('.ytp-chrome-controls')) {
            clearInterval(checkVideo);
            init();
        }
    }, 500);

   function createSpeedControl() {
    const style = document.createElement("style");
    style.textContent = `
        .ytp-speed-button {
            color: white;
            background: transparent;
            border: none;
            font-size: 14px;
            cursor: pointer;
            position: relative;
            align-self: center;
            margin-left: auto;
            margin-right: auto;
            transition: transform 0.2s ease;
        }

        .ytp-speed-menu {
            position: absolute;
            bottom: 30px;
            left: 0;
            background: #303031;
            color: white;
            border-radius: 5px;
            display: none;
            z-index: 9999;
        }

        .ytp-speed-option {
            padding: 5px 10px;
            cursor: pointer;
            font-size: 14px;
            text-align: center;
        }

        .ytp-speed-option:hover,
        .ytp-speed-option.active {
            background: Dodgerblue;
            color: #fff;
        }
    `;
    document.head.appendChild(style);

    const speeds = [0.5, 0.75, 1.0, 1.15, 1.25, 1.5, 2.0];
    let currentSpeed = parseFloat(localStorage.getItem('yt_speed') || 1.0);

    const controls = document.querySelector(".ytp-right-controls");
    if (!controls) return;

    const button = document.createElement("button");
    button.className = "ytp-speed-button";
    button.textContent = `${currentSpeed}×`;

    Object.assign(button.style, {
        color: '#fff',
        background: 'transparent',
        border: 'none',
        fontSize: '14px',
        cursor: 'pointer',
        position: 'relative',
        alignSelf: 'center',
        marginLeft: 'auto',
        marginRight: 'auto',
        transition: 'transform 0.2s ease',
    });

    const menu = document.createElement("div");
    menu.className = "ytp-speed-menu";

    speeds.forEach(speed => {
        const item = document.createElement("div");
        item.className = "ytp-speed-option";
        item.textContent = `${speed}×`;
        item.dataset.speed = speed;
        if (speed === currentSpeed) item.classList.add("active");
        menu.appendChild(item);
    });

    button.appendChild(menu);
    controls.prepend(button);

    button.addEventListener("click", () => {
        menu.style.display = menu.style.display === "block" ? "none" : "block";
    });

    menu.addEventListener("click", (e) => {
        if (e.target.classList.contains("ytp-speed-option")) {
            const newSpeed = parseFloat(e.target.dataset.speed);
            document.querySelector("video").playbackRate = newSpeed;
            currentSpeed = newSpeed;
            localStorage.setItem('yt_speed', newSpeed);
            button.firstChild.textContent = `${newSpeed}×`;
            menu.querySelectorAll(".ytp-speed-option").forEach(opt => opt.classList.remove("active"));
            e.target.classList.add("active");
            menu.style.display = "none";
        }
    });

    const video = document.querySelector("video");
    if (video) video.playbackRate = currentSpeed;
}
})();