YouTube Enhanced Player

Запоминает позицию просмотра видео и возобновляет с этого места (минус 5 секунд)

  1. // ==UserScript==
  2. // @name YouTube Enhanced Player
  3. // @name:en YouTube Enhanced Player
  4. // @name:es YouTube Reproductor Mejorado
  5. // @namespace http://tampermonkey.net/
  6. // @version 1.6
  7. // @description Запоминает позицию просмотра видео и возобновляет с этого места (минус 5 секунд)
  8. // @description:en Remembers video playback position and resumes from that point (minus 5 seconds)
  9. // @description:es Recuerda la posición de reproducción y continúa desde ese punto (menos 5 segundos)
  10. // @author YourName
  11. // @match https://www.youtube.com/*
  12. // @grant none
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // ==================== Часть 1: Возобновление воспроизведения ====================
  20. function getVideoId() {
  21. const urlParams = new URLSearchParams(window.location.search);
  22. return urlParams.get('v');
  23. }
  24. function saveVideoTime(videoId, currentTime) {
  25. localStorage.setItem(`yt_time_${videoId}`, currentTime.toString());
  26. }
  27.  
  28. function loadVideoTime(videoId) {
  29. const savedTime = localStorage.getItem(`yt_time_${videoId}`);
  30. return savedTime ? parseFloat(savedTime) : 0;
  31. }
  32. function showSaveNotification() {
  33. // 1) Находим актуальный контейнер оверлея плеера
  34. const overlay = document.querySelector('.html5-video-player .ytp-player-content')
  35. || document.querySelector('.ytp-chrome-top')
  36. || document.body;
  37.  
  38. // 2) Делаем его position: relative (если ещё не установлен)
  39. if (getComputedStyle(overlay).position === 'static') {
  40. overlay.style.position = 'relative';
  41. }
  42.  
  43. // 3) Удаляем старое уведомление (если есть)
  44. const old = overlay.querySelector('.timeSaveNotification');
  45. if (old) old.remove();
  46.  
  47. // 4) Создаём новое уведомление
  48. const notif = document.createElement('div');
  49. notif.className = 'timeSaveNotification';
  50. Object.assign(notif.style, {
  51. position: 'absolute',
  52. bottom: '0px',
  53. right: '5px',
  54. background: 'rgba(0,0,0,0.7)',
  55. color: '#fff',
  56. padding: '5px 5px',
  57. borderRadius: '5px',
  58. zIndex: '9999',
  59. fontSize: '14px',
  60. transition: 'opacity 0.3s ease',
  61. opacity: '0',
  62. });
  63. notif.innerText = 'Время просмотра сохранено!';
  64.  
  65. overlay.appendChild(notif);
  66.  
  67. // 5) Запустить анимацию «появления»
  68. // (нужен небольшой таймаут, чтобы браузер успел «нарисовать» с opacity=0)
  69. setTimeout(() => notif.style.opacity = '1', 10);
  70.  
  71. // 6) И плавно убрать через 3 секунды
  72. setTimeout(() => {
  73. notif.style.opacity = '0';
  74. setTimeout(() => {
  75. if (notif.parentNode) notif.remove();
  76. }, 300);
  77. }, 3000);
  78. }
  79.  
  80.  
  81.  
  82. function initResumePlayback() {
  83. const video = document.querySelector('video');
  84. if (!video) return;
  85.  
  86. const videoId = getVideoId();
  87. if (!videoId) return;
  88.  
  89. // Загружаем сохраненное время
  90. const savedTime = loadVideoTime(videoId);
  91. if (savedTime > 0) {
  92. // Устанавливаем время на 5 секунд раньше сохраненного
  93. const resumeTime = Math.max(0, savedTime - 5);
  94. video.currentTime = resumeTime;
  95. }
  96.  
  97. // Сохраняем время каждые 5 секунд (без уведомления)
  98. setInterval(() => {
  99. if (!video.paused) {
  100. const videoId = getVideoId();
  101. if (videoId) {
  102. localStorage.setItem(`yt_time_${videoId}`, video.currentTime.toString());
  103. }
  104. }
  105. }, 5000);
  106.  
  107. // Сохраняем время при закрытии страницы (без уведомления)
  108. window.addEventListener('beforeunload', () => {
  109. const videoId = getVideoId();
  110. if (videoId) {
  111. localStorage.setItem(`yt_time_${videoId}`, video.currentTime.toString());
  112. }
  113. });
  114. }
  115.  
  116. // ==================== Часть 2: Усилитель громкости ====================
  117. function calculateVolume(position, sliderWidth) {
  118. const volume = (position / sliderWidth) * 1400;
  119. return volume.toFixed(3);
  120. }
  121.  
  122. function updateVolumeDisplay(volume) {
  123. // Удаляем старый индикатор (если остался)
  124. const old = document.getElementById('customVolumeDisplay');
  125. if (old) old.remove();
  126.  
  127. // Находим кнопку усилителя громкости
  128. const btn = document.getElementById('volumeBoostButton');
  129. if (!btn) return;
  130.  
  131. // Создаём индикатор
  132. const volumeDisplay = document.createElement('div');
  133. volumeDisplay.id = 'customVolumeDisplay';
  134. volumeDisplay.innerText = `${volume}%`;
  135.  
  136. // Применяем ваш набор стилей
  137. Object.assign(volumeDisplay.style, {
  138. position: 'absolute',
  139. padding: '0px 0px',
  140. fontSize: '14px',
  141. background: 'rgba(0,0,0,0.8)',
  142. color: '#fff',
  143. borderRadius: '5px',
  144. whiteSpace: 'nowrap',
  145. pointerEvents: 'none',
  146. transition: 'opacity 0.2s ease',
  147. opacity: '0'
  148. });
  149.  
  150. // Вставляем в контейнер кнопки
  151. const btnContainer = btn.parentElement;
  152. btnContainer.style.position = 'relative';
  153. btnContainer.appendChild(volumeDisplay);
  154.  
  155. // Позиционируем над кнопкой
  156. const btnRect = btn.getBoundingClientRect();
  157. const containerRect = btnContainer.getBoundingClientRect();
  158. const offsetX = btnRect.left - containerRect.left + btnRect.width / 2;
  159. const offsetY = btnRect.top - containerRect.top;
  160.  
  161. volumeDisplay.style.left = `${offsetX}px`;
  162. volumeDisplay.style.top = `${offsetY}px`;
  163. volumeDisplay.style.transform = 'translate(-50%, -100%)';
  164.  
  165. // Плавно показываем
  166. requestAnimationFrame(() => {
  167. volumeDisplay.style.opacity = '1';
  168. });
  169.  
  170. // Убираем через секунду
  171. setTimeout(() => {
  172. volumeDisplay.style.opacity = '0';
  173. setTimeout(() => volumeDisplay.remove(), 200);
  174. }, 1000);
  175. }
  176.  
  177.  
  178.  
  179. // ==================== Часть 3: Создание панели управления ====================
  180. function createControlPanel(video) {
  181. const videoId = getVideoId();
  182. if (!videoId) return;
  183.  
  184. // Кнопка сохранения времени
  185. const saveButton = document.createElement('button');
  186. saveButton.id = 'manualSaveButton';
  187. saveButton.style.background = 'none';
  188. saveButton.style.border = 'none';
  189. saveButton.style.cursor = 'pointer';
  190. saveButton.style.marginRight = '5px';
  191. saveButton.innerText = '💾';
  192. saveButton.style.color = '#fff';
  193. saveButton.style.fontWeight = 'bold';
  194. saveButton.title = 'Сохранить текущее время просмотра';
  195.  
  196. // В коде кнопки сохранения нужно вызвать эту функцию:
  197. saveButton.addEventListener('click', function() {
  198. saveVideoTime(videoId, video.currentTime);
  199. showSaveNotification(); // Показываем уведомление только при нажатии
  200. });
  201.  
  202. // Кнопка усилителя громкости
  203. const volumeBoostButton = document.createElement('button');
  204. volumeBoostButton.id = 'volumeBoostButton';
  205. volumeBoostButton.style.background = 'none';
  206. volumeBoostButton.style.border = 'none';
  207. volumeBoostButton.style.cursor = 'pointer';
  208. volumeBoostButton.style.marginRight = '5px';
  209. volumeBoostButton.innerText = '🔊';
  210. volumeBoostButton.style.color = '#fff';
  211. volumeBoostButton.style.fontWeight = 'bold';
  212. volumeBoostButton.title = 'Усилитель громкости';
  213.  
  214. // Ползунок громкости
  215. const customVolumeSlider = document.createElement('input');
  216. customVolumeSlider.type = 'range';
  217. customVolumeSlider.min = '0';
  218. customVolumeSlider.max = '1400';
  219. customVolumeSlider.step = '1';
  220. customVolumeSlider.style.width = '120px';
  221. customVolumeSlider.style.display = 'none';
  222.  
  223. // Настройка AudioContext
  224. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  225. const gainNode = audioContext.createGain();
  226. gainNode.connect(audioContext.destination);
  227.  
  228. const videoSource = audioContext.createMediaElementSource(video);
  229. videoSource.connect(gainNode);
  230.  
  231. customVolumeSlider.addEventListener('input', function() {
  232. const volume = calculateVolume(this.value, this.max);
  233. gainNode.gain.value = volume / 100;
  234. updateVolumeDisplay(volume);
  235. });
  236.  
  237. function resetCustomVolumeSlider() {
  238. customVolumeSlider.value = '100';
  239. const initialVolume = calculateVolume(100, customVolumeSlider.max);
  240. gainNode.gain.value = initialVolume / 100;
  241. updateVolumeDisplay(initialVolume);
  242. }
  243.  
  244. function toggleCustomVolumeSlider() {
  245. const isSliderHidden = customVolumeSlider.style.display === 'none';
  246. customVolumeSlider.style.display = isSliderHidden ? 'block' : 'none';
  247. }
  248.  
  249. volumeBoostButton.addEventListener('click', function() {
  250. toggleCustomVolumeSlider();
  251. resetCustomVolumeSlider();
  252. });
  253.  
  254. // Вставка кнопок в один контейнер слева
  255. const controls = document.querySelector('.ytp-chrome-controls');
  256. if (controls) {
  257. const buttonContainer = document.createElement('div');
  258. buttonContainer.style.display = 'flex';
  259. buttonContainer.style.alignItems = 'center';
  260. buttonContainer.style.marginRight = '10px';
  261.  
  262. buttonContainer.appendChild(saveButton);
  263. buttonContainer.appendChild(volumeBoostButton);
  264. buttonContainer.appendChild(customVolumeSlider);
  265. controls.insertBefore(buttonContainer, controls.firstChild);
  266. buttonContainer.addEventListener('wheel', function(e) {
  267. e.preventDefault();
  268. const step = 50;
  269. let val = parseInt(customVolumeSlider.value, 10);
  270. if (e.deltaY < 0) {
  271. val = Math.min(val + step, parseInt(customVolumeSlider.max, 10));
  272. } else {
  273. val = Math.max(val - step, parseInt(customVolumeSlider.min, 10));
  274. }
  275.  
  276. customVolumeSlider.value = val;
  277. customVolumeSlider.dispatchEvent(new Event('input'));
  278. });
  279. }
  280.  
  281. resetCustomVolumeSlider();
  282. }
  283.  
  284. // ==================== Основная инициализация ====================
  285. function init() {
  286. // Инициализация возобновления воспроизведения
  287. initResumePlayback();
  288.  
  289. // Создаем панель управления
  290. const video = document.querySelector('video');
  291. if (video) {
  292. createControlPanel(video);
  293. }
  294. }
  295.  
  296. // Ждем когда видео будет готово
  297. const checkVideo = setInterval(() => {
  298. if (document.querySelector('video') && document.querySelector('.ytp-chrome-controls')) {
  299. clearInterval(checkVideo);
  300. init();
  301. }
  302. }, 500);
  303. })();