xxMrPHDxx / Youtube Teater

// ==UserScript==
// @name         Youtube Teater
// @namespace    http://tampermonkey.net/
// @version      1.0
// @license      MIT
// @description  New Look For Youtube!
// @run-at       document-idle
// @author       You
// @match        https://www.youtube.com/watch?*
// @grant        none
// ==/UserScript==

class YoutubeVideo {
  constructor(id, title, imgUrl) {
    this.id = id;
    this.title = title;
    this.image = new Image();
    this.image.src = imgUrl;
  }

  set id(value) {
    this._id = value;
  }
  get id() {
    return this._id;
  }

  set title(value) {
    this._title = value;
  }
  get title() {
    return this._title;
  }

  set image(value) {
    if (value instanceof Image) this._image = value;
  }
  get image() {
    return this._image;
  }
}

//////////////////////////////////////////////////////////////////////////////
//               Library
//////////////////////////////////////////////////////////////////////////////

function elem(tag, ...args) {
  if (typeof tag !== 'string') return;
  let [e, id, ...classes] = [document.createElement(tag), ...args];
  if (id) e.id = id;
  if (classes) e.setAttribute('class', classes.reduce((a, b) => a + " " + b, ""));
  return e;
}

function div(...args) {
  return elem('div', ...args);
}

function ul(...args) {
  return elem('ul', ...args);
}

function li(...args) {
  return elem('li', ...args);
}

function a(...args) {
  return elem('a', ...args);
}

//////////////////////////////////////////////////////////////////////////////
//               Stuff
//////////////////////////////////////////////////////////////////////////////

async function load(url) {
  let res = await fetch(url);
  return await res.text();
}

function getVideos(source) {
  return Promise.all(source.match(/(?<="compactVideoRenderer":{"videoId":").*?(?="},"longBylineText")/g));
}

function getData(matches) {
  let map = new Map;
  matches.forEach((m, i) => {
    m = m.replace(/\\u0026/g, "&");
    let id = m.split("\",\"")[0];
    let title = m.match(/(?<="simpleText":").+/g)[0];
    let thumbnail = m.match(/(?<="url":")https.+?(?=")/g)[1];
    // if(i===0) console.log(m);
    map.set(id, new YoutubeVideo(id, title, thumbnail));
  });
  return Promise.resolve(map);
}

function renderSuggestedVideos(container) {
  return function (videos) {
    videos.forEach(video => {
      let list = li(null, 'video-list');
      container.appendChild(list);

      // Clickable Link
      let videoLink = a(null, 'video-link');
      videoLink.href = `watch?v=${video.id}`;
      list.appendChild(videoLink);

      let holder = div(null, 'video-list-grid');
      videoLink.appendChild(holder);

      // Video Image
      let image = video.image;
      image.setAttribute('class', 'video-thumbnail');
      holder.appendChild(image);

      // Video Title
      let divTitle = div(null, 'video-desc');
      divTitle.innerText = video.title;
      holder.appendChild(divTitle);
    });
  }
}

//////////////////////////////////////////////////////////////////////////////
//                                  Main Stuff                              //
//////////////////////////////////////////////////////////////////////////////

let showControlsTimer;
let nextVideoTimer;

let frameHeight = 360 + 26;
let frameWidth = 640 + 26;

let style1 = `
.wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 18px 0px;
}

div.video-frame {
  width: 680px;
  height: 480px;
  background-image: url(https://az414406.vo.msecnd.net/img2/tv-transparent.svg);
  background-size: cover;
}

video {
  position: relative;
  top: 14px;
  left: 13px;
  width: 655px;
  height: 409px;
    background-color: #2C3F4F;
}

ul.video-list {
  list-style: none;
     display: inline-grid;
}

ul.video-list li {
  line-height: 110px;
    margin: 10px 0px;
}

a > *.video-list-grid {
  display: grid;
  grid-template-areas: "thumbnail desc";
}

img.video-thumbnail {
  width: 196px;
  height: 110px;
  border-radius: 10px;
}

div.video-desc {
    font-size: 22px;
    padding-left: 20px;
    padding: 10px;
}

form#vsearch {
    width: 100%;
  margin: 10px auto;
    text-align: center;
}
form#vsearch input{
  padding: 8px;
  font-size: 16px;
  border-radius: 8px;
}
form#vsearch > input[name=q]{
  width: 60%;
}
div.toggle-button-container {
  display: flex;
  flex-direction: row;
  align-items: center;
}
div.toggle-button {
  width: 100px;
  height: 30px;
    border: 1px solid #9e9e9;
  border-radius: 30px;
  background-color: #dedede;
  padding: 5px;
  margin-left: 30px;
  transition: all 0.4s;
}
div.toggle-button.slider {
  width: 40px;
  height: 40px;
  background-color: gray;
  position: relative;
  top: -10px;
  left: -10px;
  margin: 0px;
}
div.toggle-button.slider[toggled] {
  width: 40px;
  height: 40px;
  background-color: blue;
  position: relative;
  top: -10px;
  left: 60px;
  margin: 0px;
}
`;

(async function () {
  'use strict';

  let textDuration = document.body.innerHTML.substring(document.body.innerHTML.indexOf("ytp-time-duration") + 19).split("<")[0]; //document.body.innerHTML.substring(document.body.innerHTML.indexOf("duration=")).split(" ")[0].split("=")[1].replace(/\"/g,"");

  for (let prop in window) {
    if (typeof window[prop] === 'function') {
      clearInterval(window[prop]);
    }
  }

  let title = document.querySelector('title').innerText;

  let video = document.querySelector('video');
  video.removeAttribute('class');
  video.removeAttribute('style');
  video.controlsList.toggle('nodownload');
  video.download = true;

  let update = function () {
    if (!video.webkitDisplayingFullscreen) {
      video.controls = true;
      video.removeAttribute('controlslist');
      video.removeAttribute('tabindex');
      video.removeAttribute('style');
    }
    requestAnimationFrame(update);
  }

  showControlsTimer = setTimeout(update, 100);

  let source = await load(window.location);

  document.querySelector('html').style = '';
  document.head.innerHTML = `<title>${title}</title>`;
  document.body.innerHTML = `<style>${style1}</style><div class="wrapper">
<form id="vsearch" action="results?">
 <input name="q" type="text" placeholder="Search Youtube Videos"/>
 <input type="submit" value="Search"/>
</form>
<div class="video-frame">
</div>
<div id="autoplay" class="toggle-button-container">
    <span>Autoplay</span>
  <div class="toggle-button">
      <div class="toggle-button slider"></div>
  </div>
</div>
<table class="list"><tbody></tbody></table></div>`;
  let wrapper = document.querySelector('.wrapper');
  let container = wrapper.querySelector('.video-frame');
  container.appendChild(video);
  video.play();

  // Video List Stuff
  const videoListHolder = div();
  wrapper.appendChild(videoListHolder);
  const videoList = ul(null, 'video-list');
  videoListHolder.appendChild(videoList);

  let nextVideo = null;
  getVideos(source).then(getData).then(videos => {
    renderSuggestedVideos(videoList)(videos);
    nextVideo = videos.entries().next().value[1];
  });

  let time = textDuration.split(/:/g);
  let s = time.map(t => parseInt(t)).reduce((a, b, i, arr) => a + b * Math.pow(60, arr.length - i - 1), 0);
  let ignoredTime = isNaN(s);

  console.log(`Text Time: ${textDuration}, Time: ${time.reduce((a,b)=>a+":"+b)}, Video Length: ${s} seconds`);

  // Autoplay next video
  let checkForNextVideo = function () {
    let duration = video.getDuration() | 0 + 1;
    if (ignoredTime || Math.abs(duration - s) > 1) return;
    if (video.getDuration() - video.getCurrentTime() < 0.01) {
      if (typeof nextVideo !== 'undefined') {
        let a = document.createElement("a");
        a.href = `watch?v=${nextVideo.id}`;
        a.click();
      }
    }
  }

  document.querySelectorAll("div.toggle-button-container").forEach(container => {
    let toggle_button = container.querySelector("div.toggle-button.slider");
    container.addEventListener("click", function (event) {
      if (toggle_button.hasAttribute("toggled")) {
        toggle_button.removeAttribute("toggled");
        localStorage.setItem("autoplay", "0");
        try {
          toggle_button.onuntoggled(event);
        }
        catch (e) {}
      }
      else {
        toggle_button.setAttribute("toggled", "");
        localStorage.setItem("autoplay", "1");
        try {
          toggle_button.ontoggled(event);
        }
        catch (e) {}
      }
    });
    toggle_button.ontoggled = function (e) {
      nextVideoTimer = setInterval(checkForNextVideo, 100);
    }
    toggle_button.onuntoggled = function (e) {
      clearInterval(nextVideoTimer);
    }
  });

  // Setup autoplay and its toggle button to match localStorage if it exists
  let autoplay = localStorage.getItem("autoplay");
  if (autoplay !== null) {
    let toggle_button = document.querySelector("div#autoplay div.toggle-button.slider");
    try {
      if (autoplay === "1") {
        toggle_button.setAttribute("toggled", "");
        localStorage.setItem("autoplay", "1");
        try {
          toggle_button.ontoggled(event);
        }
        catch (e) {}
      }
      else if (autoplay === "0") {
        toggle_button.removeAttribute("toggled");
        localStorage.setItem("autoplay", "0");
        try {
          toggle_button.onuntoggled(event);
        }
        catch (e) {}
      }
    }
    catch (e) {}
  }
  else {
    localStorage.setItem("autoplay", "0");
  }
})();