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.3.1
  7. // @description Запоминает позицию просмотра видео и громкость, возобновляет с этого места (минус 5 секунд)
  8. // @description:en Remembers video playback position and volume, resumes from that point (minus 5 seconds)
  9. // @description:es Recuerda la posición y volumen de reproducción, continúa desde ese punto (menos 5 segundos)
  10. // @author LegonYY
  11. // @match https://www.youtube.com/*
  12. // @grant none
  13. // @icon https://img.icons8.com/?size=100&id=55200&format=png&color=000000
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. function getVideoId() {
  21. const urlParams = new URLSearchParams(window.location.search);
  22. return urlParams.get('v');
  23. }
  24.  
  25. function saveVideoTime(videoId, currentTime) {
  26. localStorage.setItem(`yt_time_${videoId}`, currentTime.toString());
  27. }
  28.  
  29. function loadVideoTime(videoId) {
  30. const savedTime = localStorage.getItem(`yt_time_${videoId}`);
  31. return savedTime ? parseFloat(savedTime) : 0;
  32. }
  33.  
  34. function saveVolumeLevel(volume) {
  35. localStorage.setItem('yt_volume_global', volume.toString());
  36. }
  37.  
  38. function loadVolumeLevel() {
  39. const savedVolume = localStorage.getItem('yt_volume_global');
  40. return savedVolume ? parseFloat(savedVolume) : 100;
  41. }
  42.  
  43. function showSaveNotification() {
  44. const overlay = document.querySelector('.html5-video-player .ytp-player-content')
  45. || document.querySelector('.ytp-chrome-top')
  46. || document.body;
  47.  
  48. if (getComputedStyle(overlay).position === 'static') {
  49. overlay.style.position = 'relative';
  50. }
  51.  
  52. const old = overlay.querySelector('.timeSaveNotification');
  53. if (old) old.remove();
  54.  
  55. const notif = document.createElement('div');
  56. notif.className = 'timeSaveNotification';
  57. Object.assign(notif.style, {
  58. position: 'absolute',
  59. bottom: '0px',
  60. right: '5px',
  61. background: 'rgba(0,0,0,0.7)',
  62. color: '#fff',
  63. padding: '5px 10px',
  64. borderRadius: '5px',
  65. zIndex: '9999',
  66. fontSize: '14px',
  67. opacity: '0',
  68. transition: 'opacity 0.5s ease',
  69. });
  70. notif.innerText = 'Время просмотра сохранено!';
  71. overlay.appendChild(notif);
  72. requestAnimationFrame(() => notif.style.opacity = '1');
  73. setTimeout(() => {
  74. notif.style.opacity = '0';
  75. setTimeout(() => notif.remove(), 500);
  76. }, 3000);
  77. }
  78.  
  79. function initResumePlayback() {
  80. const video = document.querySelector('video');
  81. if (!video) return;
  82.  
  83. const videoId = getVideoId();
  84. if (!videoId) return;
  85.  
  86. const savedTime = loadVideoTime(videoId);
  87.  
  88.  
  89. video.addEventListener('loadedmetadata', () => {
  90. if (savedTime > 0 && video.duration > savedTime - 5) {
  91. const resumeTime = Math.max(0, savedTime - 5);
  92. video.currentTime = resumeTime;
  93. }
  94. });
  95.  
  96. setInterval(() => {
  97. if (!video.paused && !video.seeking) {
  98. const videoId = getVideoId();
  99. if (videoId) {
  100. saveVideoTime(videoId, video.currentTime);
  101. }
  102. }
  103. }, 5000);
  104.  
  105. window.addEventListener('beforeunload', () => {
  106. const videoId = getVideoId();
  107. if (videoId) {
  108. saveVideoTime(videoId, video.currentTime);
  109. }
  110. });
  111. }
  112.  
  113.  
  114. function calculateVolume(position, sliderMax) {
  115. const volume = (position / sliderMax) * 1400;
  116. return volume.toFixed();
  117. }
  118.  
  119. function updateVolumeDisplay(volume) {
  120. const old = document.getElementById('customVolumeDisplay');
  121. if (old) old.remove();
  122.  
  123. const btn = document.getElementById('volumeBoostButton');
  124. if (!btn) return;
  125.  
  126. const volumeDisplay = document.createElement('div');
  127. volumeDisplay.id = 'customVolumeDisplay';
  128. volumeDisplay.innerText = `${volume}%`;
  129.  
  130. Object.assign(volumeDisplay.style, {
  131. position: 'absolute',
  132. fontSize: '14px',
  133. background: 'rgba(0,0,0,0.8)',
  134. color: '#fff',
  135. borderRadius: '5px',
  136. whiteSpace: 'nowrap',
  137. padding: '2px 6px',
  138. pointerEvents: 'none',
  139. transition: 'opacity 0.3s ease, transform 0.3s ease',
  140. opacity: '0',
  141. transform: 'translate(-50%, -10px)',
  142. });
  143.  
  144. const btnContainer = btn.parentElement;
  145. btnContainer.style.position = 'relative';
  146. btnContainer.appendChild(volumeDisplay);
  147.  
  148. const btnRect = btn.getBoundingClientRect();
  149. const containerRect = btnContainer.getBoundingClientRect();
  150. const offsetX = btnRect.left - containerRect.left + btnRect.width / 2;
  151. const offsetY = btnRect.top - containerRect.top;
  152.  
  153. volumeDisplay.style.left = `${offsetX}px`;
  154. volumeDisplay.style.top = `${offsetY}px`;
  155.  
  156. requestAnimationFrame(() => {
  157. volumeDisplay.style.opacity = '1';
  158. volumeDisplay.style.transform = 'translate(-50%, -20px)';
  159. });
  160.  
  161. setTimeout(() => {
  162. volumeDisplay.style.opacity = '0';
  163. volumeDisplay.style.transform = 'translate(-50%, -10px)';
  164. setTimeout(() => volumeDisplay.remove(), 300);
  165. }, 1000);
  166. }
  167.  
  168. function createControlPanel(video) {
  169. const style = document.createElement('style');
  170. style.textContent = `
  171. #volumeBoostButton input[type=range] {
  172. -webkit-appearance: none;
  173. width: 100px;
  174. height: 4px;
  175. background: #ccc;
  176. border-radius: 2px;
  177. outline: none;
  178. }
  179. #volumeBoostButton input[type=range]::-webkit-slider-thumb {
  180. -webkit-appearance: none;
  181. appearance: none;
  182. width: 12px;
  183. height: 12px;
  184. border-radius: 50%;
  185. background: #fff;
  186. cursor: pointer;
  187. box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
  188. }`;
  189. document.head.appendChild(style);
  190.  
  191. const saveButton = document.createElement('button');
  192. saveButton.id = 'manualSaveButton';
  193. saveButton.innerText = '💾';
  194. Object.assign(saveButton.style, {
  195. background: 'none',
  196. border: 'none',
  197. cursor: 'pointer',
  198. color: '#fff',
  199. fontWeight: 'bold',
  200. marginRight: '1px',
  201. fontSize: '18px',
  202. transition: 'transform 0.2s ease',
  203. });
  204. saveButton.title = 'Сохранить текущее время просмотра';
  205.  
  206. const volumeBoostButton = document.createElement('button');
  207. volumeBoostButton.id = 'volumeBoostButton';
  208. volumeBoostButton.innerText = '🔊';
  209. Object.assign(volumeBoostButton.style, {
  210. background: 'none',
  211. border: 'none',
  212. cursor: 'pointer',
  213. color: '#fff',
  214. fontWeight: 'bold',
  215. marginRight: '1px',
  216. fontSize: '18px',
  217. transition: 'transform 0.2s ease',
  218. });
  219. volumeBoostButton.title = 'Усилитель громкости';
  220.  
  221. const customVolumeSlider = document.createElement('input');
  222. Object.assign(customVolumeSlider, {
  223. type: 'range',
  224. min: '100',
  225. max: '1400',
  226. step: '1',
  227. });
  228.  
  229. Object.assign(customVolumeSlider.style, {
  230. display: 'none',
  231. opacity: '0',
  232. transform: 'scale(0.8)',
  233. transition: 'opacity 0.3s ease, transform 0.3s ease',
  234. });
  235.  
  236. const audioContext = new (window.AudioContext || window.webkitAudioContext)();
  237. const gainNode = audioContext.createGain();
  238. gainNode.connect(audioContext.destination);
  239. const videoSource = audioContext.createMediaElementSource(video);
  240. videoSource.connect(gainNode);
  241.  
  242. const initialVolume = loadVolumeLevel();
  243. gainNode.gain.value = initialVolume / 100;
  244. customVolumeSlider.value = initialVolume.toString();
  245. updateVolumeDisplay(initialVolume.toString());
  246.  
  247. customVolumeSlider.addEventListener('input', function () {
  248. const volume = calculateVolume(this.value, this.max);
  249. gainNode.gain.value = volume / 100;
  250. updateVolumeDisplay(volume);
  251. saveVolumeLevel(volume);
  252. });
  253.  
  254. function resetVolumeTo100() {
  255. customVolumeSlider.value = '100';
  256. gainNode.gain.value = 1.0;
  257. updateVolumeDisplay('100');
  258. saveVolumeLevel(100);
  259. }
  260.  
  261. volumeBoostButton.addEventListener('mouseenter', () => {
  262. customVolumeSlider.style.display = 'block';
  263. requestAnimationFrame(() => {
  264. customVolumeSlider.style.opacity = '1';
  265. customVolumeSlider.style.transform = 'scale(1)';
  266. });
  267. });
  268.  
  269. volumeBoostButton.addEventListener('click', (e) => {
  270. e.stopPropagation();
  271. resetVolumeTo100();
  272. });
  273.  
  274. let hideTimeout;
  275. const sliderContainer = document.createElement('div');
  276. sliderContainer.style.display = 'flex';
  277. sliderContainer.style.alignItems = 'center';
  278. sliderContainer.style.position = 'relative';
  279. sliderContainer.style.cursor = 'pointer';
  280.  
  281. sliderContainer.addEventListener('mouseleave', () => {
  282. hideTimeout = setTimeout(() => {
  283. customVolumeSlider.style.opacity = '0';
  284. customVolumeSlider.style.transform = 'scale(0.8)';
  285. setTimeout(() => {
  286. customVolumeSlider.style.display = 'none';
  287. }, 300);
  288. }, 300);
  289. });
  290.  
  291. sliderContainer.addEventListener('mouseenter', () => {
  292. clearTimeout(hideTimeout);
  293. });
  294.  
  295. const controls = document.querySelector('.ytp-chrome-controls');
  296. if (controls) {
  297. const buttonContainer = document.createElement('div');
  298. buttonContainer.style.display = 'flex';
  299. buttonContainer.style.alignItems = 'center';
  300. buttonContainer.style.marginRight = '10px';
  301.  
  302. sliderContainer.appendChild(volumeBoostButton);
  303. sliderContainer.appendChild(customVolumeSlider);
  304. buttonContainer.appendChild(saveButton);
  305. buttonContainer.appendChild(sliderContainer);
  306. controls.insertBefore(buttonContainer, controls.firstChild);
  307.  
  308. sliderContainer.addEventListener('wheel', (e) => {
  309. e.preventDefault();
  310. const step = 50;
  311. let val = parseInt(customVolumeSlider.value, 10);
  312. if (e.deltaY < 0) {
  313. val = Math.min(val + step, parseInt(customVolumeSlider.max, 10));
  314. } else {
  315. val = Math.max(val - step, parseInt(customVolumeSlider.min, 10));
  316. }
  317. customVolumeSlider.value = val;
  318. customVolumeSlider.dispatchEvent(new Event('input'));
  319. });
  320. }
  321.  
  322. saveButton.addEventListener('click', () => {
  323. const videoId = getVideoId();
  324. if (videoId) {
  325. saveVideoTime(videoId, video.currentTime);
  326. showSaveNotification();
  327. }
  328. });
  329. }
  330.  
  331. function init() {
  332. initResumePlayback();
  333. const video = document.querySelector('video');
  334. if (video) {
  335. createControlPanel(video);
  336. createSpeedControl();
  337. }
  338. }
  339.  
  340.  
  341. const checkVideo = setInterval(() => {
  342. if (document.querySelector('video') && document.querySelector('.ytp-chrome-controls')) {
  343. clearInterval(checkVideo);
  344. init();
  345. }
  346. }, 500);
  347.  
  348. function createSpeedControl() {
  349. const style = document.createElement("style");
  350. style.textContent = `
  351. .ytp-speed-button {
  352. color: white;
  353. background: transparent;
  354. border: none;
  355. font-size: 14px;
  356. cursor: pointer;
  357. position: relative;
  358. align-self: center;
  359. margin-left: auto;
  360. margin-right: auto;
  361. transition: transform 0.2s ease;
  362. }
  363.  
  364. .ytp-speed-menu {
  365. position: absolute;
  366. bottom: 30px;
  367. left: 0;
  368. background: #303031;
  369. color: white;
  370. border-radius: 5px;
  371. display: none;
  372. z-index: 9999;
  373. }
  374.  
  375. .ytp-speed-option {
  376. padding: 5px 10px;
  377. cursor: pointer;
  378. font-size: 14px;
  379. text-align: center;
  380. }
  381.  
  382. .ytp-speed-option:hover,
  383. .ytp-speed-option.active {
  384. background: Dodgerblue;
  385. color: #fff;
  386. }
  387. `;
  388. document.head.appendChild(style);
  389.  
  390. const speeds = [0.5, 0.75, 1.0, 1.15, 1.25, 1.5, 2.0];
  391. let currentSpeed = parseFloat(localStorage.getItem('yt_speed') || 1.0);
  392.  
  393. const controls = document.querySelector(".ytp-right-controls");
  394. if (!controls) return;
  395.  
  396. const button = document.createElement("button");
  397. button.className = "ytp-speed-button";
  398. button.textContent = `${currentSpeed}×`;
  399.  
  400. Object.assign(button.style, {
  401. color: '#fff',
  402. background: 'transparent',
  403. border: 'none',
  404. fontSize: '14px',
  405. cursor: 'pointer',
  406. position: 'relative',
  407. alignSelf: 'center',
  408. marginLeft: 'auto',
  409. marginRight: 'auto',
  410. transition: 'transform 0.2s ease',
  411. });
  412.  
  413. const menu = document.createElement("div");
  414. menu.className = "ytp-speed-menu";
  415.  
  416. speeds.forEach(speed => {
  417. const item = document.createElement("div");
  418. item.className = "ytp-speed-option";
  419. item.textContent = `${speed}×`;
  420. item.dataset.speed = speed;
  421. if (speed === currentSpeed) item.classList.add("active");
  422. menu.appendChild(item);
  423. });
  424.  
  425. button.appendChild(menu);
  426. controls.prepend(button);
  427.  
  428. button.addEventListener("click", () => {
  429. menu.style.display = menu.style.display === "block" ? "none" : "block";
  430. });
  431.  
  432. menu.addEventListener("click", (e) => {
  433. if (e.target.classList.contains("ytp-speed-option")) {
  434. const newSpeed = parseFloat(e.target.dataset.speed);
  435. document.querySelector("video").playbackRate = newSpeed;
  436. currentSpeed = newSpeed;
  437. localStorage.setItem('yt_speed', newSpeed);
  438. button.firstChild.textContent = `${newSpeed}×`;
  439. menu.querySelectorAll(".ytp-speed-option").forEach(opt => opt.classList.remove("active"));
  440. e.target.classList.add("active");
  441. menu.style.display = "none";
  442. }
  443. });
  444.  
  445. const video = document.querySelector("video");
  446. if (video) video.playbackRate = currentSpeed;
  447. }
  448. })();