mirror of
				https://github.com/iv-org/invidious.git
				synced 2025-11-04 06:08:31 -06:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/master' into side-menu
This commit is contained in:
		
						commit
						31b587baea
					
				@ -58,6 +58,7 @@ div {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  animation: spin 2s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -80,11 +81,15 @@ a.pure-button-primary:hover {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.thumbnail {
 | 
			
		||||
  padding: 28.125%;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img.thumbnail {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
}
 | 
			
		||||
@ -255,6 +260,41 @@ img.thumbnail {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vjs-play-control,
 | 
			
		||||
.vjs-volume-panel,
 | 
			
		||||
.vjs-current-time,
 | 
			
		||||
.vjs-time-control,
 | 
			
		||||
.vjs-duration,
 | 
			
		||||
.vjs-progress-control,
 | 
			
		||||
.vjs-remaining-time {
 | 
			
		||||
  order: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vjs-captions-button {
 | 
			
		||||
  order: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vjs-quality-selector {
 | 
			
		||||
  order: 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vjs-playback-rate {
 | 
			
		||||
  order: 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vjs-share-control {
 | 
			
		||||
  order: 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vjs-fullscreen-control {
 | 
			
		||||
  order: 6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.vjs-control-bar {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.video-js .vjs-control-bar,
 | 
			
		||||
.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
 | 
			
		||||
  background-color: rgba(35, 35, 35, 0.75);
 | 
			
		||||
@ -326,29 +366,17 @@ img.thumbnail {
 | 
			
		||||
  padding-top: 82vh;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
video.video-js {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#player-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding-bottom: 82vh;
 | 
			
		||||
  height: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#progress-container {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  background-color: #a0a0a0;
 | 
			
		||||
  color: rgba(35, 35, 35, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#download-progress {
 | 
			
		||||
  width: 0%;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  height: 10px;
 | 
			
		||||
  background-color: rgba(0, 182, 240, 1);
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  margin-top: 0.5em;
 | 
			
		||||
  margin-bottom: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pure-control-group label {
 | 
			
		||||
  word-wrap: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								assets/css/video-js.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								assets/css/video-js.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
/**
 | 
			
		||||
 * videojs-share
 | 
			
		||||
 * @version 2.0.1
 | 
			
		||||
 * @version 3.0.0
 | 
			
		||||
 * @copyright 2018 Mikhail Khazov <mkhazov.work@gmail.com>
 | 
			
		||||
 * @license MIT
 | 
			
		||||
 */
 | 
			
		||||
.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-modal-dialog-content{display:flex;align-items:center;padding:0;background-image:linear-gradient(to bottom, rgba(0,0,0,0.77), rgba(0,0,0,0.75))}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{position:absolute;right:0;top:5px;width:30px;height:30px;color:#fff;cursor:pointer;opacity:0.9;transition:opacity 0.25s ease-out}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:before{content:'×';font-size:20px;line-height:15px}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:hover{opacity:1}.video-js .vjs-share{display:flex;flex-direction:column;justify-content:space-around;align-items:center;width:100%;height:100%;max-height:400px}.video-js .vjs-share__top,.video-js .vjs-share__middle,.video-js .vjs-share__bottom{display:flex}.video-js .vjs-share__top,.video-js .vjs-share__middle{flex-direction:column;justify-content:space-between}.video-js .vjs-share__middle{padding:0 25px}.video-js .vjs-share__title{align-self:center;font-size:22px;color:#fff}.video-js .vjs-share__subtitle{width:100%;margin:0 auto 12px;font-size:16px;color:#fff;opacity:0.7}.video-js .vjs-share__short-link-wrapper{position:relative;display:block;width:100%;height:40px;margin:0 auto;margin-bottom:15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none;overflow:hidden;flex-shrink:0}.video-js .vjs-share__short-link{display:block;width:100%;height:100%;padding:0 40px 0 15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none}.video-js .vjs-share__btn{position:absolute;right:0;bottom:0;height:40px;width:40px;display:flex;align-items:center;padding:0 11px;border:0;color:#fff;background-color:#2e2e2e;background-size:18px 19px;background-position:center;background-repeat:no-repeat;cursor:pointer;outline:none;transition:width 0.3s ease-out, padding 0.3s ease-out}.video-js .vjs-share__btn svg{flex-shrink:0}.video-js .vjs-share__btn span{position:relative;padding-left:10px;opacity:0;transition:opacity 0.3s ease-out}.video-js .vjs-share__btn:hover{justify-content:center;width:100%;padding:0 40px;background-image:none}.video-js .vjs-share__btn:hover span{opacity:1}.video-js .vjs-share__socials{display:flex;flex-wrap:wrap;justify-content:center;align-content:flex-start;transition:width 0.3s ease-out, height 0.3s ease-out}.video-js .vjs-share__social{display:flex;justify-content:center;align-items:center;flex-shrink:0;width:32px;height:32px;margin-right:6px;margin-bottom:6px;cursor:pointer;font-size:8px;transition:transform 0.3s ease-out, filter 0.2s ease-out;border:none;outline:none}.video-js .vjs-share__social:hover{filter:brightness(115%)}.video-js .vjs-share__social svg{width:100%;max-height:24px}.video-js .vjs-share__social_vk{background-color:#5d7294}.video-js .vjs-share__social_ok{background-color:#ed7c20}.video-js .vjs-share__social_mail{background-color:#134785}.video-js .vjs-share__social_tw{background-color:#76aaeb}.video-js .vjs-share__social_reddit{background-color:#ff4500}.video-js .vjs-share__social_fbFeed{background-color:#475995}.video-js .vjs-share__social_messenger{background-color:#0084ff}.video-js .vjs-share__social_gp{background-color:#d53f35}.video-js .vjs-share__social_linkedin{background-color:#0077b5}.video-js .vjs-share__social_viber{background-color:#766db5}.video-js .vjs-share__social_telegram{background-color:#4bb0e2}.video-js .vjs-share__social_whatsapp{background-color:#78c870}.video-js .vjs-share__bottom{justify-content:center}@media (max-height: 220px){.video-js .vjs-share .hidden-xs{display:none}}@media (max-height: 350px){.video-js .vjs-share .hidden-sm{display:none}}@media (min-height: 400px){.video-js .vjs-share__title{margin-bottom:15px}.video-js .vjs-share__short-link-wrapper{margin-bottom:30px}}@media (min-width: 320px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:5px;top:10px}}@media (min-width: 660px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:20px;top:20px}.video-js .vjs-share__social{width:40px;height:40px}}
 | 
			
		||||
.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-modal-dialog-content{display:flex;align-items:center;padding:0;background-image:linear-gradient(to bottom, rgba(0,0,0,0.77), rgba(0,0,0,0.75))}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{position:absolute;right:0;top:5px;width:30px;height:30px;color:#fff;cursor:pointer;opacity:0.9;transition:opacity 0.25s ease-out}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:before{content:'×';font-size:20px;line-height:15px}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:hover{opacity:1}.video-js .vjs-share{display:flex;flex-direction:column;justify-content:space-around;align-items:center;width:100%;height:100%;max-height:400px}.video-js .vjs-share__top,.video-js .vjs-share__middle,.video-js .vjs-share__bottom{display:flex}.video-js .vjs-share__top,.video-js .vjs-share__middle{flex-direction:column;justify-content:space-between}.video-js .vjs-share__middle{padding:0 25px}.video-js .vjs-share__title{align-self:center;font-size:22px;color:#fff}.video-js .vjs-share__subtitle{width:100%;margin:0 auto 12px;font-size:16px;color:#fff;opacity:0.7}.video-js .vjs-share__short-link-wrapper{position:relative;display:block;width:100%;height:40px;margin:0 auto;margin-bottom:15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none;overflow:hidden;flex-shrink:0}.video-js .vjs-share__short-link{display:block;width:100%;height:100%;padding:0 40px 0 15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none}.video-js .vjs-share__btn{position:absolute;right:0;bottom:0;height:40px;width:40px;display:flex;align-items:center;padding:0 11px;border:0;color:#fff;background-color:#2e2e2e;background-size:18px 19px;background-position:center;background-repeat:no-repeat;cursor:pointer;outline:none;transition:width 0.3s ease-out, padding 0.3s ease-out}.video-js .vjs-share__btn svg{flex-shrink:0}.video-js .vjs-share__btn span{position:relative;padding-left:10px;opacity:0;transition:opacity 0.3s ease-out}.video-js .vjs-share__btn:hover{justify-content:center;width:100%;padding:0 40px;background-image:none}.video-js .vjs-share__btn:hover span{opacity:1}.video-js .vjs-share__socials{display:flex;flex-wrap:wrap;justify-content:center;align-content:flex-start;transition:width 0.3s ease-out, height 0.3s ease-out}.video-js .vjs-share__social{display:flex;justify-content:center;align-items:center;flex-shrink:0;width:32px;height:32px;margin-right:6px;margin-bottom:6px;cursor:pointer;font-size:8px;transition:transform 0.3s ease-out, filter 0.2s ease-out;border:none;outline:none}.video-js .vjs-share__social:hover{filter:brightness(115%)}.video-js .vjs-share__social svg{overflow:visible;max-height:24px}.video-js .vjs-share__social_vk{background-color:#5d7294}.video-js .vjs-share__social_ok{background-color:#ed7c20}.video-js .vjs-share__social_mail{background-color:#134785}.video-js .vjs-share__social_tw{background-color:#76aaeb}.video-js .vjs-share__social_reddit{background-color:#ff4500}.video-js .vjs-share__social_fbFeed{background-color:#475995}.video-js .vjs-share__social_messenger{background-color:#0084ff}.video-js .vjs-share__social_gp{background-color:#d53f35}.video-js .vjs-share__social_linkedin{background-color:#0077b5}.video-js .vjs-share__social_viber{background-color:#766db5}.video-js .vjs-share__social_telegram{background-color:#4bb0e2}.video-js .vjs-share__social_whatsapp{background-color:#78c870}.video-js .vjs-share__bottom{justify-content:center}@media (max-height: 220px){.video-js .vjs-share .hidden-xs{display:none}}@media (max-height: 350px){.video-js .vjs-share .hidden-sm{display:none}}@media (min-height: 400px){.video-js .vjs-share__title{margin-bottom:15px}.video-js .vjs-share__short-link-wrapper{margin-bottom:30px}}@media (min-width: 320px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:5px;top:10px}}@media (min-width: 660px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:20px;top:20px}.video-js .vjs-share__social{width:40px;height:40px}}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										19
									
								
								assets/js/video.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								assets/js/video.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										4
									
								
								assets/js/videojs-share.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								assets/js/videojs-share.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,413 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Video.js Hotkeys
 | 
			
		||||
 * https://github.com/ctd1500/videojs-hotkeys
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (c) 2015 Chris Dougherty
 | 
			
		||||
 * Licensed under the Apache-2.0 license.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
;(function(root, factory) {
 | 
			
		||||
  if (typeof window !== 'undefined' && window.videojs) {
 | 
			
		||||
    factory(window.videojs);
 | 
			
		||||
  } else if (typeof define === 'function' && define.amd) {
 | 
			
		||||
    define('videojs-hotkeys', ['video.js'], function (module) {
 | 
			
		||||
      return factory(module.default || module);
 | 
			
		||||
    });
 | 
			
		||||
  } else if (typeof module !== 'undefined' && module.exports) {
 | 
			
		||||
    module.exports = factory(require('video.js'));
 | 
			
		||||
  }
 | 
			
		||||
}(this, function (videojs) {
 | 
			
		||||
  "use strict";
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    window['videojs_hotkeys'] = { version: "0.2.22" };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var hotkeys = function(options) {
 | 
			
		||||
    var player = this;
 | 
			
		||||
    var pEl = player.el();
 | 
			
		||||
    var doc = document;
 | 
			
		||||
    var def_options = {
 | 
			
		||||
      volumeStep: 0.1,
 | 
			
		||||
      seekStep: 5,
 | 
			
		||||
      enableMute: true,
 | 
			
		||||
      enableVolumeScroll: true,
 | 
			
		||||
      enableHoverScroll: true,
 | 
			
		||||
      enableFullscreen: true,
 | 
			
		||||
      enableNumbers: true,
 | 
			
		||||
      enableJogStyle: false,
 | 
			
		||||
      alwaysCaptureHotkeys: false,
 | 
			
		||||
      enableModifiersForNumbers: true,
 | 
			
		||||
      enableInactiveFocus: true,
 | 
			
		||||
      skipInitialFocus: false,
 | 
			
		||||
      playPauseKey: playPauseKey,
 | 
			
		||||
      rewindKey: rewindKey,
 | 
			
		||||
      forwardKey: forwardKey,
 | 
			
		||||
      volumeUpKey: volumeUpKey,
 | 
			
		||||
      volumeDownKey: volumeDownKey,
 | 
			
		||||
      muteKey: muteKey,
 | 
			
		||||
      fullscreenKey: fullscreenKey,
 | 
			
		||||
      customKeys: {}
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var cPlay = 1,
 | 
			
		||||
      cRewind = 2,
 | 
			
		||||
      cForward = 3,
 | 
			
		||||
      cVolumeUp = 4,
 | 
			
		||||
      cVolumeDown = 5,
 | 
			
		||||
      cMute = 6,
 | 
			
		||||
      cFullscreen = 7;
 | 
			
		||||
 | 
			
		||||
    // Use built-in merge function from Video.js v5.0+ or v4.4.0+
 | 
			
		||||
    var mergeOptions = videojs.mergeOptions || videojs.util.mergeOptions;
 | 
			
		||||
    options = mergeOptions(def_options, options || {});
 | 
			
		||||
 | 
			
		||||
    var volumeStep = options.volumeStep,
 | 
			
		||||
      seekStep = options.seekStep,
 | 
			
		||||
      enableMute = options.enableMute,
 | 
			
		||||
      enableVolumeScroll = options.enableVolumeScroll,
 | 
			
		||||
      enableHoverScroll = options.enableHoverScroll,
 | 
			
		||||
      enableFull = options.enableFullscreen,
 | 
			
		||||
      enableNumbers = options.enableNumbers,
 | 
			
		||||
      enableJogStyle = options.enableJogStyle,
 | 
			
		||||
      alwaysCaptureHotkeys = options.alwaysCaptureHotkeys,
 | 
			
		||||
      enableModifiersForNumbers = options.enableModifiersForNumbers,
 | 
			
		||||
      enableInactiveFocus = options.enableInactiveFocus,
 | 
			
		||||
      skipInitialFocus = options.skipInitialFocus;
 | 
			
		||||
 | 
			
		||||
    // Set default player tabindex to handle keydown and doubleclick events
 | 
			
		||||
    if (!pEl.hasAttribute('tabIndex')) {
 | 
			
		||||
      pEl.setAttribute('tabIndex', '-1');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove player outline to fix video performance issue
 | 
			
		||||
    pEl.style.outline = "none";
 | 
			
		||||
 | 
			
		||||
    if (alwaysCaptureHotkeys || !player.autoplay()) {
 | 
			
		||||
      if (!skipInitialFocus) {
 | 
			
		||||
        player.one('play', function() {
 | 
			
		||||
          pEl.focus(); // Fixes the .vjs-big-play-button handing focus back to body instead of the player
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (enableInactiveFocus) {
 | 
			
		||||
      player.on('userinactive', function() {
 | 
			
		||||
        // When the control bar fades, re-apply focus to the player if last focus was a control button
 | 
			
		||||
        var cancelFocusingPlayer = function() {
 | 
			
		||||
          clearTimeout(focusingPlayerTimeout);
 | 
			
		||||
        };
 | 
			
		||||
        var focusingPlayerTimeout = setTimeout(function() {
 | 
			
		||||
          player.off('useractive', cancelFocusingPlayer);
 | 
			
		||||
          var activeElement = doc.activeElement;
 | 
			
		||||
          var controlBar = pEl.querySelector('.vjs-control-bar');
 | 
			
		||||
          if (activeElement && activeElement.parentElement == controlBar) {
 | 
			
		||||
            pEl.focus();
 | 
			
		||||
          }
 | 
			
		||||
        }, 10);
 | 
			
		||||
 | 
			
		||||
        player.one('useractive', cancelFocusingPlayer);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    player.on('play', function() {
 | 
			
		||||
      // Fix allowing the YouTube plugin to have hotkey support.
 | 
			
		||||
      var ifblocker = pEl.querySelector('.iframeblocker');
 | 
			
		||||
      if (ifblocker && ifblocker.style.display === '') {
 | 
			
		||||
        ifblocker.style.display = "block";
 | 
			
		||||
        ifblocker.style.bottom = "39px";
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    var keyDown = function keyDown(event) {
 | 
			
		||||
      var ewhich = event.which, wasPlaying, seekTime;
 | 
			
		||||
      var ePreventDefault = event.preventDefault;
 | 
			
		||||
      var duration = player.duration();
 | 
			
		||||
      // When controls are disabled, hotkeys will be disabled as well
 | 
			
		||||
      if (player.controls()) {
 | 
			
		||||
 | 
			
		||||
        // Don't catch keys if any control buttons are focused, unless alwaysCaptureHotkeys is true
 | 
			
		||||
        var activeEl = doc.activeElement;
 | 
			
		||||
        if (alwaysCaptureHotkeys ||
 | 
			
		||||
            activeEl == pEl ||
 | 
			
		||||
            activeEl == pEl.querySelector('.vjs-tech') ||
 | 
			
		||||
            activeEl == pEl.querySelector('.vjs-control-bar') ||
 | 
			
		||||
            activeEl == pEl.querySelector('.iframeblocker')) {
 | 
			
		||||
 | 
			
		||||
          switch (checkKeys(event, player)) {
 | 
			
		||||
            // Spacebar toggles play/pause
 | 
			
		||||
            case cPlay:
 | 
			
		||||
              ePreventDefault();
 | 
			
		||||
              if (alwaysCaptureHotkeys) {
 | 
			
		||||
                // Prevent control activation with space
 | 
			
		||||
                event.stopPropagation();
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (player.paused()) {
 | 
			
		||||
                player.play();
 | 
			
		||||
              } else {
 | 
			
		||||
                player.pause();
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            // Seeking with the left/right arrow keys
 | 
			
		||||
            case cRewind: // Seek Backward
 | 
			
		||||
              wasPlaying = !player.paused();
 | 
			
		||||
              ePreventDefault();
 | 
			
		||||
              if (wasPlaying) {
 | 
			
		||||
                player.pause();
 | 
			
		||||
              }
 | 
			
		||||
              seekTime = player.currentTime() - seekStepD(event);
 | 
			
		||||
              // The flash player tech will allow you to seek into negative
 | 
			
		||||
              // numbers and break the seekbar, so try to prevent that.
 | 
			
		||||
              if (seekTime <= 0) {
 | 
			
		||||
                seekTime = 0;
 | 
			
		||||
              }
 | 
			
		||||
              player.currentTime(seekTime);
 | 
			
		||||
              if (wasPlaying) {
 | 
			
		||||
                player.play();
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
            case cForward: // Seek Forward
 | 
			
		||||
              wasPlaying = !player.paused();
 | 
			
		||||
              ePreventDefault();
 | 
			
		||||
              if (wasPlaying) {
 | 
			
		||||
                player.pause();
 | 
			
		||||
              }
 | 
			
		||||
              seekTime = player.currentTime() + seekStepD(event);
 | 
			
		||||
              // Fixes the player not sending the end event if you
 | 
			
		||||
              // try to seek past the duration on the seekbar.
 | 
			
		||||
              if (seekTime >= duration) {
 | 
			
		||||
                seekTime = wasPlaying ? duration - .001 : duration;
 | 
			
		||||
              }
 | 
			
		||||
              player.currentTime(seekTime);
 | 
			
		||||
              if (wasPlaying) {
 | 
			
		||||
                player.play();
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            // Volume control with the up/down arrow keys
 | 
			
		||||
            case cVolumeDown:
 | 
			
		||||
              ePreventDefault();
 | 
			
		||||
              if (!enableJogStyle) {
 | 
			
		||||
                player.volume(player.volume() - volumeStep);
 | 
			
		||||
              } else {
 | 
			
		||||
                seekTime = player.currentTime() - 1;
 | 
			
		||||
                if (player.currentTime() <= 1) {
 | 
			
		||||
                  seekTime = 0;
 | 
			
		||||
                }
 | 
			
		||||
                player.currentTime(seekTime);
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
            case cVolumeUp:
 | 
			
		||||
              ePreventDefault();
 | 
			
		||||
              if (!enableJogStyle) {
 | 
			
		||||
                player.volume(player.volume() + volumeStep);
 | 
			
		||||
              } else {
 | 
			
		||||
                seekTime = player.currentTime() + 1;
 | 
			
		||||
                if (seekTime >= duration) {
 | 
			
		||||
                  seekTime = duration;
 | 
			
		||||
                }
 | 
			
		||||
                player.currentTime(seekTime);
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            // Toggle Mute with the M key
 | 
			
		||||
            case cMute:
 | 
			
		||||
              if (enableMute) {
 | 
			
		||||
                player.muted(!player.muted());
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            // Toggle Fullscreen with the F key
 | 
			
		||||
            case  cFullscreen:
 | 
			
		||||
              if (enableFull) {
 | 
			
		||||
                if (player.isFullscreen()) {
 | 
			
		||||
                  player.exitFullscreen();
 | 
			
		||||
                } else {
 | 
			
		||||
                  player.requestFullscreen();
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
              // Number keys from 0-9 skip to a percentage of the video. 0 is 0% and 9 is 90%
 | 
			
		||||
              if ((ewhich > 47 && ewhich < 59) || (ewhich > 95 && ewhich < 106)) {
 | 
			
		||||
                // Do not handle if enableModifiersForNumbers set to false and keys are Ctrl, Cmd or Alt
 | 
			
		||||
                if (enableModifiersForNumbers || !(event.metaKey || event.ctrlKey || event.altKey)) {
 | 
			
		||||
                  if (enableNumbers) {
 | 
			
		||||
                    var sub = 48;
 | 
			
		||||
                    if (ewhich > 95) {
 | 
			
		||||
                      sub = 96;
 | 
			
		||||
                    }
 | 
			
		||||
                    var number = ewhich - sub;
 | 
			
		||||
                    ePreventDefault();
 | 
			
		||||
                    player.currentTime(player.duration() * number * 0.1);
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              // Handle any custom hotkeys
 | 
			
		||||
              for (var customKey in options.customKeys) {
 | 
			
		||||
                var customHotkey = options.customKeys[customKey];
 | 
			
		||||
                // Check for well formed custom keys
 | 
			
		||||
                if (customHotkey && customHotkey.key && customHotkey.handler) {
 | 
			
		||||
                  // Check if the custom key's condition matches
 | 
			
		||||
                  if (customHotkey.key(event)) {
 | 
			
		||||
                    ePreventDefault();
 | 
			
		||||
                    customHotkey.handler(player, options, event);
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var doubleClick = function doubleClick(event) {
 | 
			
		||||
      // When controls are disabled, hotkeys will be disabled as well
 | 
			
		||||
      if (player.controls()) {
 | 
			
		||||
 | 
			
		||||
        // Don't catch clicks if any control buttons are focused
 | 
			
		||||
        var activeEl = event.relatedTarget || event.toElement || doc.activeElement;
 | 
			
		||||
        if (activeEl == pEl ||
 | 
			
		||||
            activeEl == pEl.querySelector('.vjs-tech') ||
 | 
			
		||||
            activeEl == pEl.querySelector('.iframeblocker')) {
 | 
			
		||||
 | 
			
		||||
          if (enableFull) {
 | 
			
		||||
            if (player.isFullscreen()) {
 | 
			
		||||
              player.exitFullscreen();
 | 
			
		||||
            } else {
 | 
			
		||||
              player.requestFullscreen();
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var volumeHover = false;
 | 
			
		||||
    var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel');
 | 
			
		||||
    volumeSelector.onmouseover = function() { volumeHover = true; }
 | 
			
		||||
    volumeSelector.onmouseout = function() { volumeHover = false; }
 | 
			
		||||
    
 | 
			
		||||
    var mouseScroll = function mouseScroll(event) {
 | 
			
		||||
      if (enableHoverScroll) {
 | 
			
		||||
        // If we leave this undefined then it can match non-existent elements below
 | 
			
		||||
        var activeEl = 0;
 | 
			
		||||
      } else {
 | 
			
		||||
        var activeEl = doc.activeElement;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // When controls are disabled, hotkeys will be disabled as well
 | 
			
		||||
      if (player.controls()) {
 | 
			
		||||
        if (alwaysCaptureHotkeys ||
 | 
			
		||||
            activeEl == pEl ||
 | 
			
		||||
            activeEl == pEl.querySelector('.vjs-tech') ||
 | 
			
		||||
            activeEl == pEl.querySelector('.iframeblocker') ||
 | 
			
		||||
            activeEl == pEl.querySelector('.vjs-control-bar') ||
 | 
			
		||||
            volumeHover) {
 | 
			
		||||
 | 
			
		||||
          if (enableVolumeScroll) {
 | 
			
		||||
            event = window.event || event;
 | 
			
		||||
            var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
 | 
			
		||||
            event.preventDefault();
 | 
			
		||||
 | 
			
		||||
            if (delta == 1) {
 | 
			
		||||
              player.volume(player.volume() + volumeStep);
 | 
			
		||||
            } else if (delta == -1) {
 | 
			
		||||
              player.volume(player.volume() - volumeStep);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var checkKeys = function checkKeys(e, player) {
 | 
			
		||||
      // Allow some modularity in defining custom hotkeys
 | 
			
		||||
 | 
			
		||||
      // Play/Pause check
 | 
			
		||||
      if (options.playPauseKey(e, player)) {
 | 
			
		||||
        return cPlay;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Seek Backward check
 | 
			
		||||
      if (options.rewindKey(e, player)) {
 | 
			
		||||
        return cRewind;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Seek Forward check
 | 
			
		||||
      if (options.forwardKey(e, player)) {
 | 
			
		||||
        return cForward;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Volume Up check
 | 
			
		||||
      if (options.volumeUpKey(e, player)) {
 | 
			
		||||
        return cVolumeUp;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Volume Down check
 | 
			
		||||
      if (options.volumeDownKey(e, player)) {
 | 
			
		||||
        return cVolumeDown;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Mute check
 | 
			
		||||
      if (options.muteKey(e, player)) {
 | 
			
		||||
        return cMute;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Fullscreen check
 | 
			
		||||
      if (options.fullscreenKey(e, player)) {
 | 
			
		||||
        return cFullscreen;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function playPauseKey(e) {
 | 
			
		||||
      // Space bar or MediaPlayPause
 | 
			
		||||
      return (e.which === 32 || e.which === 179);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function rewindKey(e) {
 | 
			
		||||
      // Left Arrow or MediaRewind
 | 
			
		||||
      return (e.which === 37 || e.which === 177);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function forwardKey(e) {
 | 
			
		||||
      // Right Arrow or MediaForward
 | 
			
		||||
      return (e.which === 39 || e.which === 176);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function volumeUpKey(e) {
 | 
			
		||||
      // Up Arrow
 | 
			
		||||
      return (e.which === 38);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function volumeDownKey(e) {
 | 
			
		||||
      // Down Arrow
 | 
			
		||||
      return (e.which === 40);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function muteKey(e) {
 | 
			
		||||
      // M key
 | 
			
		||||
      return (e.which === 77);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function fullscreenKey(e) {
 | 
			
		||||
      // F key
 | 
			
		||||
      return (e.which === 70);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function seekStepD(e) {
 | 
			
		||||
      // SeekStep caller, returns an int, or a function returning an int
 | 
			
		||||
      return (typeof seekStep === "function" ? seekStep(e) : seekStep);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    player.on('keydown', keyDown);
 | 
			
		||||
    player.on('dblclick', doubleClick);
 | 
			
		||||
    player.on('mousewheel', mouseScroll);
 | 
			
		||||
    player.on("DOMMouseScroll", mouseScroll);
 | 
			
		||||
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  var registerPlugin = videojs.registerPlugin || videojs.plugin;
 | 
			
		||||
  registerPlugin('hotkeys', hotkeys);
 | 
			
		||||
}));
 | 
			
		||||
							
								
								
									
										5
									
								
								assets/js/videojs.hotkeys.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								assets/js/videojs.hotkeys.min.js
									
									
									
									
										vendored
									
									
								
							@ -1,2 +1,3 @@
 | 
			
		||||
/* videojs-hotkeys v0.2.22 - https://github.com/ctd1500/videojs-hotkeys */
 | 
			
		||||
!function(e,t){"undefined"!=typeof window&&window.videojs?t(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return t(e.default||e)}):"undefined"!=typeof module&&module.exports&&(module.exports=t(require("video.js")))}(0,function(s){"use strict";"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.22"});(s.registerPlugin||s.plugin)("hotkeys",function(m){var y=this,v=y.el(),f=document,e={volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!0,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},t=s.mergeOptions||s.util.mergeOptions,d=(m=t(e,m||{})).volumeStep,n=m.seekStep,p=m.enableMute,r=m.enableVolumeScroll,o=m.enableHoverScroll,b=m.enableFullscreen,h=m.enableNumbers,w=m.enableJogStyle,k=m.alwaysCaptureHotkeys,S=m.enableModifiersForNumbers,u=m.enableInactiveFocus,l=m.skipInitialFocus;v.hasAttribute("tabIndex")||v.setAttribute("tabIndex","-1"),v.style.outline="none",!k&&y.autoplay()||l||y.one("play",function(){v.focus()}),u&&y.on("userinactive",function(){var n=function(){clearTimeout(e)},e=setTimeout(function(){y.off("useractive",n);var e=f.activeElement,t=v.querySelector(".vjs-control-bar");e&&e.parentElement==t&&v.focus()},10);y.one("useractive",n)}),y.on("play",function(){var e=v.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")});var i=!1,c=v.querySelector(".vjs-volume-menu-button")||v.querySelector(".vjs-volume-panel");c.onmouseover=function(){i=!0},c.onmouseout=function(){i=!1};var a=function(e){if(o)var t=0;else t=f.activeElement;if(y.controls()&&(k||t==v||t==v.querySelector(".vjs-tech")||t==v.querySelector(".iframeblocker")||t==v.querySelector(".vjs-control-bar")||i)&&r){e=window.event||e;var n=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail));e.preventDefault(),1==n?y.volume(y.volume()+d):-1==n&&y.volume(y.volume()-d)}},K=function(e,t){return m.playPauseKey(e,t)?1:m.rewindKey(e,t)?2:m.forwardKey(e,t)?3:m.volumeUpKey(e,t)?4:m.volumeDownKey(e,t)?5:m.muteKey(e,t)?6:m.fullscreenKey(e,t)?7:void 0};function q(e){return"function"==typeof n?n(e):n}return y.on("keydown",function(e){var t,n,r=e.which,o=e.preventDefault,u=y.duration();if(y.controls()){var l=f.activeElement;if(k||l==v||l==v.querySelector(".vjs-tech")||l==v.querySelector(".vjs-control-bar")||l==v.querySelector(".iframeblocker"))switch(K(e,y)){case 1:o(),k&&e.stopPropagation(),y.paused()?y.play():y.pause();break;case 2:t=!y.paused(),o(),t&&y.pause(),(n=y.currentTime()-q(e))<=0&&(n=0),y.currentTime(n),t&&y.play();break;case 3:t=!y.paused(),o(),t&&y.pause(),u<=(n=y.currentTime()+q(e))&&(n=t?u-.001:u),y.currentTime(n),t&&y.play();break;case 5:o(),w?(n=y.currentTime()-1,y.currentTime()<=1&&(n=0),y.currentTime(n)):y.volume(y.volume()-d);break;case 4:o(),w?(u<=(n=y.currentTime()+1)&&(n=u),y.currentTime(n)):y.volume(y.volume()+d);break;case 6:p&&y.muted(!y.muted());break;case 7:b&&(y.isFullscreen()?y.exitFullscreen():y.requestFullscreen());break;default:if((47<r&&r<59||95<r&&r<106)&&(S||!(e.metaKey||e.ctrlKey||e.altKey))&&h){var i=48;95<r&&(i=96);var c=r-i;o(),y.currentTime(y.duration()*c*.1)}for(var a in m.customKeys){var s=m.customKeys[a];s&&s.key&&s.handler&&s.key(e)&&(o(),s.handler(y,m,e))}}}}),y.on("dblclick",function(e){if(y.controls()){var t=e.relatedTarget||e.toElement||f.activeElement;t!=v&&t!=v.querySelector(".vjs-tech")&&t!=v.querySelector(".iframeblocker")||b&&(y.isFullscreen()?y.exitFullscreen():y.requestFullscreen())}}),y.on("mousewheel",a),y.on("DOMMouseScroll",a),this})});
 | 
			
		||||
/* videojs-hotkeys v0.2.25 - https://github.com/ctd1500/videojs-hotkeys */
 | 
			
		||||
!function(e,n){"undefined"!=typeof window&&window.videojs?n(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return n(e.default||e)}):"undefined"!=typeof module&&module.exports&&(module.exports=n(require("video.js")))}(0,function(e){"use strict";"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.25"});(e.registerPlugin||e.plugin)("hotkeys",function(n){function t(e){return"function"==typeof s?s(e):s}function r(e){null!=e&&"function"==typeof e.then&&e.then(null,function(e){})}var o=this,u=o.el(),l=document,i={volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!1,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},c=e.mergeOptions||e.util.mergeOptions,a=(n=c(i,n||{})).volumeStep,s=n.seekStep,m=n.enableMute,f=n.enableVolumeScroll,y=n.enableHoverScroll,v=n.enableFullscreen,d=n.enableNumbers,p=n.enableJogStyle,b=n.alwaysCaptureHotkeys,h=n.enableModifiersForNumbers,w=n.enableInactiveFocus,k=n.skipInitialFocus,S=e.VERSION;u.hasAttribute("tabIndex")||u.setAttribute("tabIndex","-1"),u.style.outline="none",!b&&o.autoplay()||k||o.one("play",function(){u.focus()}),w&&o.on("userinactive",function(){var e=function(){clearTimeout(n)},n=setTimeout(function(){o.off("useractive",e);var n=l.activeElement,t=u.querySelector(".vjs-control-bar");n&&n.parentElement==t&&u.focus()},10);o.one("useractive",e)}),o.on("play",function(){var e=u.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")});var K=!1,q=u.querySelector(".vjs-volume-menu-button")||u.querySelector(".vjs-volume-panel");null!=q&&(q.onmouseover=function(){K=!0},q.onmouseout=function(){K=!1});var j=function(e){if(y)n=0;else var n=l.activeElement;if(o.controls()&&(b||n==u||n==u.querySelector(".vjs-tech")||n==u.querySelector(".iframeblocker")||n==u.querySelector(".vjs-control-bar")||K)&&f){e=window.event||e;var t=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail));e.preventDefault(),1==t?o.volume(o.volume()+a):-1==t&&o.volume(o.volume()-a)}},F=function(e,t){return n.playPauseKey(e,t)?1:n.rewindKey(e,t)?2:n.forwardKey(e,t)?3:n.volumeUpKey(e,t)?4:n.volumeDownKey(e,t)?5:n.muteKey(e,t)?6:n.fullscreenKey(e,t)?7:void 0};return o.on("keydown",function(e){var i,c,s=e.which,f=e.preventDefault,y=o.duration();if(o.controls()){var w=l.activeElement;if(b||w==u||w==u.querySelector(".vjs-tech")||w==u.querySelector(".vjs-control-bar")||w==u.querySelector(".iframeblocker"))switch(F(e,o)){case 1:f(),b&&e.stopPropagation(),o.paused()?r(o.play()):o.pause();break;case 2:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()-t(e))<=0&&(c=0),o.currentTime(c),i&&r(o.play());break;case 3:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()+t(e))>=y&&(c=i?y-.001:y),o.currentTime(c),i&&r(o.play());break;case 5:f(),p?(c=o.currentTime()-1,o.currentTime()<=1&&(c=0),o.currentTime(c)):o.volume(o.volume()-a);break;case 4:f(),p?((c=o.currentTime()+1)>=y&&(c=y),o.currentTime(c)):o.volume(o.volume()+a);break;case 6:m&&o.muted(!o.muted());break;case 7:v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen());break;default:if((s>47&&s<59||s>95&&s<106)&&(h||!(e.metaKey||e.ctrlKey||e.altKey))&&d){var k=48;s>95&&(k=96);var S=s-k;f(),o.currentTime(o.duration()*S*.1)}for(var K in n.customKeys){var q=n.customKeys[K];q&&q.key&&q.handler&&q.key(e)&&(f(),q.handler(o,n,e))}}}}),o.on("dblclick",function(e){if(null!=S&&S<="7.1.0"&&o.controls()){var n=e.relatedTarget||e.toElement||l.activeElement;n!=u&&n!=u.querySelector(".vjs-tech")&&n!=u.querySelector(".iframeblocker")||v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen())}}),o.on("mousewheel",j),o.on("DOMMouseScroll",j),this})});
 | 
			
		||||
//# sourceMappingURL=videojs.hotkeys.min.js.map
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
video_threads: 0
 | 
			
		||||
crawl_threads: 0
 | 
			
		||||
channel_threads: 1
 | 
			
		||||
feed_threads: 1
 | 
			
		||||
db:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								config/migrate-scripts/migrate-db-1c8075c.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								config/migrate-scripts/migrate-db-1c8075c.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
psql invidious -c "ALTER TABLE channel_videos DROP COLUMN live_now CASCADE"
 | 
			
		||||
psql invidious -c "ALTER TABLE channel_videos DROP COLUMN premiere_timestamp CASCADE"
 | 
			
		||||
 | 
			
		||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN live_now bool"
 | 
			
		||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz"
 | 
			
		||||
							
								
								
									
										4
									
								
								config/migrate-scripts/migrate-db-6e51189.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								config/migrate-scripts/migrate-db-6e51189.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN live_now bool;"
 | 
			
		||||
psql invidious -c "UPDATE channel_videos SET live_now = false;"
 | 
			
		||||
							
								
								
									
										3
									
								
								config/migrate-scripts/migrate-db-88b7097.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								config/migrate-scripts/migrate-db-88b7097.sh
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz;"
 | 
			
		||||
@ -11,6 +11,8 @@ CREATE TABLE public.channel_videos
 | 
			
		||||
  ucid text,
 | 
			
		||||
  author text,
 | 
			
		||||
  length_seconds integer,
 | 
			
		||||
  live_now boolean,
 | 
			
		||||
  premiere_timestamp timestamp with time zone,
 | 
			
		||||
  CONSTRAINT channel_videos_id_key UNIQUE (id)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
  "Clear watch history?": "Êtes-vous sûr de vouloir supprimer l'historique des vidéos regardées ?",
 | 
			
		||||
  "Yes": "Oui",
 | 
			
		||||
  "No": "Non",
 | 
			
		||||
  "Import and Export Data": "Importer et Exporter les Données",
 | 
			
		||||
  "Import and Export Data": "Importer et exporter des données",
 | 
			
		||||
  "Import": "Importer",
 | 
			
		||||
  "Import Invidious data": "Importer des données Invidious",
 | 
			
		||||
  "Import YouTube subscriptions": "Importer des abonnements YouTube",
 | 
			
		||||
@ -45,19 +45,19 @@
 | 
			
		||||
  "Email:": "E-mail :",
 | 
			
		||||
  "Google verification code:": "Code de vérification Google :",
 | 
			
		||||
  "Preferences": "Préférences",
 | 
			
		||||
  "Player preferences": "Préférences du Lecteur",
 | 
			
		||||
  "Player preferences": "Préférences du lecteur",
 | 
			
		||||
  "Always loop: ": "Lire en boucle : ",
 | 
			
		||||
  "Autoplay: ": "Lire Automatiquement : ",
 | 
			
		||||
  "Autoplay: ": "Lire automatiquement : ",
 | 
			
		||||
  "Autoplay next video: ": "Lire automatiquement la vidéo suivante : ",
 | 
			
		||||
  "Listen by default: ": "Audio Uniquement par défaut : ",
 | 
			
		||||
  "Proxy videos? ": "Souhaitez vous charger les vidéos à travers un proxy ?",
 | 
			
		||||
  "Listen by default: ": "Audio uniquement : ",
 | 
			
		||||
  "Proxy videos? ": "Charger les vidéos à travers un proxy ? ",
 | 
			
		||||
  "Default speed: ": "Vitesse par défaut : ",
 | 
			
		||||
  "Preferred video quality: ": "Qualité vidéo souhaitée : ",
 | 
			
		||||
  "Player volume: ": "Volume du lecteur : ",
 | 
			
		||||
  "Default comments: ": "Source des Commentaires : ",
 | 
			
		||||
  "Default captions: ": "Sous-titres principal : ",
 | 
			
		||||
  "Fallback captions: ": "Sous-titres secondaire : ",
 | 
			
		||||
  "Show related videos? ": "Voir les vidéos liées à ce sujet ? ",
 | 
			
		||||
  "Default comments: ": "Source des commentaires : ",
 | 
			
		||||
  "Default captions: ": "Sous-titres par défaut : ",
 | 
			
		||||
  "Fallback captions: ": "Fallback captions: ",
 | 
			
		||||
  "Show related videos? ": "Voir les vidéos liées ? ",
 | 
			
		||||
  "Visual preferences": "Préférences du site",
 | 
			
		||||
  "Dark mode: ": "Mode Sombre : ",
 | 
			
		||||
  "Thin mode: ": "Mode Simplifié : ",
 | 
			
		||||
@ -82,13 +82,13 @@
 | 
			
		||||
  "Watch history": "Historique de visionnage",
 | 
			
		||||
  "Delete account": "Supprimer votre compte",
 | 
			
		||||
  "Administrator preferences": "Préferences d'Administrateur",
 | 
			
		||||
  "Default homepage: ": "Page d'accueil par defaut :",
 | 
			
		||||
  "Feed menu: ": "Menu des Flux :",
 | 
			
		||||
  "Top enabled? ": "Top activé ?",
 | 
			
		||||
  "CAPTCHA enabled? ": "CAPTCHA activé ?",
 | 
			
		||||
  "Login enabled? ": "Connexion activé ?",
 | 
			
		||||
  "Registration enabled? ": "Inscription activé ?",
 | 
			
		||||
  "Report statistics? ": "Telemetrie activé ?",
 | 
			
		||||
  "Default homepage: ": "Page d'accueil par défaut : ",
 | 
			
		||||
  "Feed menu: ": "Menu des Flux : ",
 | 
			
		||||
  "Top enabled? ": "Top activé ? ",
 | 
			
		||||
  "CAPTCHA enabled? ": "CAPTCHA activé ? ",
 | 
			
		||||
  "Login enabled? ": "Connexion activé ? ",
 | 
			
		||||
  "Registration enabled? ": "Inscription activée ? ",
 | 
			
		||||
  "Report statistics? ": "Télémétrie activé ? ",
 | 
			
		||||
  "Save preferences": "Enregistrer les préférences",
 | 
			
		||||
  "Subscription manager": "Gestionnaire d'abonnement",
 | 
			
		||||
  "`x` subscriptions": "`x` abonnements",
 | 
			
		||||
@ -108,11 +108,11 @@
 | 
			
		||||
  "License: ": "Licence : ",
 | 
			
		||||
  "Family friendly? ": "Tout Public ? ",
 | 
			
		||||
  "Wilson score: ": "Score de Wilson : ",
 | 
			
		||||
  "Engagement: ": "Poucentage de spectateur aillant aimé Liker ou Disliker la vidéo : ",
 | 
			
		||||
  "Engagement: ": "Poucentage de spectateur aillant aimé Like ou Dislike la vidéo : ",
 | 
			
		||||
  "Whitelisted regions: ": "Régions en liste blanche : ",
 | 
			
		||||
  "Blacklisted regions: ": "Régions sur liste noire : ",
 | 
			
		||||
  "Shared `x`": "Partagée `x`",
 | 
			
		||||
  "Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Il semblerait que JavaScript sois désactivé. Cliquez ici pour voir les commentaires. Gardez à l'esprit que le chargement peut prendre plus de temps.",
 | 
			
		||||
  "Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Il semblerait que JavaScript soit désactivé. Cliquez ici pour voir les commentaires sans. Gardez à l'esprit que le chargement peut prendre plus de temps.",
 | 
			
		||||
  "View YouTube comments": "Voir les commentaires YouTube",
 | 
			
		||||
  "View more comments on Reddit": "Voir plus de commentaires sur Reddit",
 | 
			
		||||
  "View `x` comments": "Voir `x` commentaires",
 | 
			
		||||
@ -124,11 +124,11 @@
 | 
			
		||||
  "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Si vous ne parvenez pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.",
 | 
			
		||||
  "Invalid TFA code": "Code d'authentification à deux facteurs invalide",
 | 
			
		||||
  "Login failed. This may be because two-factor authentication is not enabled on your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.",
 | 
			
		||||
  "Invalid answer": "Réponse non valide",
 | 
			
		||||
  "Invalid answer": "Réponse invalide",
 | 
			
		||||
  "Invalid CAPTCHA": "CAPTCHA invalide",
 | 
			
		||||
  "CAPTCHA is a required field": "Veuillez rentrez un CAPTCHA",
 | 
			
		||||
  "User ID is a required field": "Veuillez rentrez un Identifiant Utilisateur",
 | 
			
		||||
  "Password is a required field": "Veuillez rentrez un Mot de passe",
 | 
			
		||||
  "CAPTCHA is a required field": "Veuillez entrer un CAPTCHA",
 | 
			
		||||
  "User ID is a required field": "Veuillez entrer un Identifiant Utilisateur",
 | 
			
		||||
  "Password is a required field": "Veuillez entrer un Mot de passe",
 | 
			
		||||
  "Invalid username or password": "Nom d'utilisateur ou mot de passe invalide",
 | 
			
		||||
  "Please sign in using 'Sign in with Google'": "Veuillez vous connecter en utilisant \"S'identifier avec Google\"",
 | 
			
		||||
  "Password cannot be empty": "Le mot de passe ne peut pas être vide",
 | 
			
		||||
@ -268,7 +268,7 @@
 | 
			
		||||
  "`x` hours": "`x` heures",
 | 
			
		||||
  "`x` minutes": "`x` minutes",
 | 
			
		||||
  "`x` seconds": "`x` secondes",
 | 
			
		||||
  "Fallback comments: ": "Commentaires secondaires : ",
 | 
			
		||||
  "Fallback comments: ": "Fallback comments: ",
 | 
			
		||||
  "Popular": "Populaire",
 | 
			
		||||
  "Top": "Top",
 | 
			
		||||
  "About": "A Propos",
 | 
			
		||||
@ -289,5 +289,5 @@
 | 
			
		||||
  "Video mode": "Mode Vidéo",
 | 
			
		||||
  "Videos": "Vidéos",
 | 
			
		||||
  "Playlists": "Liste de lecture",
 | 
			
		||||
  "Current version: ": "Version actuelle :"
 | 
			
		||||
  "Current version: ": "Version :"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
    "newest": "najnowsze",
 | 
			
		||||
    "oldest": "najstarsze",
 | 
			
		||||
    "popular": "popularne",
 | 
			
		||||
  "last": "",
 | 
			
		||||
    "last": "ostatnie",
 | 
			
		||||
    "Next page": "Następna strona",
 | 
			
		||||
    "Previous page": "Poprzednia strona",
 | 
			
		||||
    "Clear watch history?": "Wyczyścić historię?",
 | 
			
		||||
@ -50,7 +50,7 @@
 | 
			
		||||
    "Autoplay: ": "Autoodtwarzanie: ",
 | 
			
		||||
    "Autoplay next video: ": "Odtwórz następny film: ",
 | 
			
		||||
    "Listen by default: ": "Tryb dźwiękowy: ",
 | 
			
		||||
  "Proxy videos? ": "",
 | 
			
		||||
    "Proxy videos? ": "Filmy przez proxy? ",
 | 
			
		||||
    "Default speed: ": "Domyślna prędkość: ",
 | 
			
		||||
    "Preferred video quality: ": "Preferowana jakość filmów: ",
 | 
			
		||||
    "Player volume: ": "Głośność odtwarzacza: ",
 | 
			
		||||
@ -101,7 +101,7 @@
 | 
			
		||||
    "Released under the AGPLv3 by Omar Roth.": "Wydano na licencji AGPLv3 przez Omar Roth.",
 | 
			
		||||
    "Source available here.": "Kod źródłowy dostępny tutaj.",
 | 
			
		||||
    "View JavaScript license information.": "Wyświetl informację o licencji JavaScript.",
 | 
			
		||||
  "View privacy policy.": "",
 | 
			
		||||
    "View privacy policy.": "Polityka prywatności.",
 | 
			
		||||
    "Trending": "Na czasie",
 | 
			
		||||
    "Watch video on Youtube": "Zobacz film na YouTube",
 | 
			
		||||
    "Genre: ": "Gatunek: ",
 | 
			
		||||
@ -270,7 +270,7 @@
 | 
			
		||||
    "`x` seconds": "`x` sekund",
 | 
			
		||||
    "Fallback comments: ": "Zastępcze komentarze: ",
 | 
			
		||||
    "Popular": "Popularne",
 | 
			
		||||
  "Top": "Na czasie",
 | 
			
		||||
    "Top": "Najczęściej oglądane",
 | 
			
		||||
    "About": "Informacje",
 | 
			
		||||
    "Rating: ": "Ocena: ",
 | 
			
		||||
    "Language: ": "Język: ",
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ name: invidious
 | 
			
		||||
version: 0.15.0
 | 
			
		||||
 | 
			
		||||
authors:
 | 
			
		||||
  - Omar Roth <omarroth@hotmail.com>
 | 
			
		||||
  - Omar Roth <omarroth@protonmail.com>
 | 
			
		||||
 | 
			
		||||
targets:
 | 
			
		||||
  invidious:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										466
									
								
								src/invidious.cr
									
									
									
									
									
								
							
							
						
						
									
										466
									
								
								src/invidious.cr
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -17,6 +17,8 @@ class ChannelVideo
 | 
			
		||||
    ucid:               String,
 | 
			
		||||
    author:             String,
 | 
			
		||||
    length_seconds:     {type: Int32, default: 0},
 | 
			
		||||
    live_now:           {type: Bool, default: false},
 | 
			
		||||
    premiere_timestamp: {type: Time?, default: nil},
 | 
			
		||||
  })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -117,10 +119,27 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
 | 
			
		||||
      author = entry.xpath_node("author/name").not_nil!.content
 | 
			
		||||
      ucid = entry.xpath_node("channelid").not_nil!.content
 | 
			
		||||
 | 
			
		||||
      length_seconds = videos.select { |video| video.id == video_id }[0]?.try &.length_seconds
 | 
			
		||||
      channel_video = videos.select { |video| video.id == video_id }[0]?
 | 
			
		||||
 | 
			
		||||
      length_seconds = channel_video.try &.length_seconds
 | 
			
		||||
      length_seconds ||= 0
 | 
			
		||||
 | 
			
		||||
      video = ChannelVideo.new(video_id, title, published, Time.now, ucid, author, length_seconds)
 | 
			
		||||
      live_now = channel_video.try &.live_now
 | 
			
		||||
      live_now ||= false
 | 
			
		||||
 | 
			
		||||
      premiere_timestamp = channel_video.try &.premiere_timestamp
 | 
			
		||||
 | 
			
		||||
      video = ChannelVideo.new(
 | 
			
		||||
        video_id,
 | 
			
		||||
        title,
 | 
			
		||||
        published,
 | 
			
		||||
        Time.now,
 | 
			
		||||
        ucid,
 | 
			
		||||
        author,
 | 
			
		||||
        length_seconds,
 | 
			
		||||
        live_now,
 | 
			
		||||
        premiere_timestamp
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      db.exec("UPDATE users SET notifications = notifications || $1 \
 | 
			
		||||
        WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, ucid)
 | 
			
		||||
@ -128,9 +147,12 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
 | 
			
		||||
      video_array = video.to_a
 | 
			
		||||
      args = arg_array(video_array)
 | 
			
		||||
 | 
			
		||||
      # We don't include the 'premire_timestamp' here because channel pages don't include them,
 | 
			
		||||
      # meaning the above timestamp is always null
 | 
			
		||||
      db.exec("INSERT INTO channel_videos VALUES (#{args}) \
 | 
			
		||||
      ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
 | 
			
		||||
      updated = $4, ucid = $5, author = $6, length_seconds = $7", video_array)
 | 
			
		||||
      updated = $4, ucid = $5, author = $6, length_seconds = $7, \
 | 
			
		||||
      live_now = $8", video_array)
 | 
			
		||||
    end
 | 
			
		||||
  else
 | 
			
		||||
    page = 1
 | 
			
		||||
@ -157,7 +179,17 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      count = nodeset.size
 | 
			
		||||
      videos = videos.map { |video| ChannelVideo.new(video.id, video.title, video.published, Time.now, video.ucid, video.author, video.length_seconds) }
 | 
			
		||||
      videos = videos.map { |video| ChannelVideo.new(
 | 
			
		||||
        video.id,
 | 
			
		||||
        video.title,
 | 
			
		||||
        video.published,
 | 
			
		||||
        Time.now,
 | 
			
		||||
        video.ucid,
 | 
			
		||||
        video.author,
 | 
			
		||||
        video.length_seconds,
 | 
			
		||||
        video.live_now,
 | 
			
		||||
        video.premiere_timestamp
 | 
			
		||||
      ) }
 | 
			
		||||
 | 
			
		||||
      videos.each do |video|
 | 
			
		||||
        ids << video.id
 | 
			
		||||
@ -170,8 +202,12 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
 | 
			
		||||
          video_array = video.to_a
 | 
			
		||||
          args = arg_array(video_array)
 | 
			
		||||
 | 
			
		||||
          db.exec("INSERT INTO channel_videos VALUES (#{args}) ON CONFLICT (id) DO UPDATE SET title = $2, \
 | 
			
		||||
          published = $3, updated = $4, ucid = $5, author = $6, length_seconds = $7", video_array)
 | 
			
		||||
          # We don't include the 'premire_timestamp' here because channel pages don't include them,
 | 
			
		||||
          # meaning the above timestamp is always null
 | 
			
		||||
          db.exec("INSERT INTO channel_videos VALUES (#{args}) \
 | 
			
		||||
          ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
 | 
			
		||||
          updated = $4, ucid = $5, author = $6, length_seconds = $7, \
 | 
			
		||||
          live_now = $8", video_array)
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										133
									
								
								src/invidious/helpers/handlers.cr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/invidious/helpers/handlers.cr
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,133 @@
 | 
			
		||||
module HTTP::Handler
 | 
			
		||||
  @@exclude_routes_tree = Radix::Tree(String).new
 | 
			
		||||
 | 
			
		||||
  macro exclude(paths, method = "GET")
 | 
			
		||||
      class_name = {{@type.name}}
 | 
			
		||||
      method_downcase = {{method.downcase}}
 | 
			
		||||
      class_name_method = "#{class_name}/#{method_downcase}"
 | 
			
		||||
      ({{paths}}).each do |path|
 | 
			
		||||
        @@exclude_routes_tree.add class_name_method + path, '/' + method_downcase + path
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
  def exclude_match?(env : HTTP::Server::Context)
 | 
			
		||||
    @@exclude_routes_tree.find(radix_path(env.request.method, env.request.path)).found?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private def radix_path(method : String, path : String)
 | 
			
		||||
    "#{self.class}/#{method.downcase}#{path}"
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class Kemal::RouteHandler
 | 
			
		||||
  exclude ["/api/v1/*"]
 | 
			
		||||
 | 
			
		||||
  # Processes the route if it's a match. Otherwise renders 404.
 | 
			
		||||
  private def process_request(context)
 | 
			
		||||
    raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
 | 
			
		||||
    content = context.route.handler.call(context)
 | 
			
		||||
 | 
			
		||||
    if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)
 | 
			
		||||
      raise Kemal::Exceptions::CustomException.new(context)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context.response.print(content)
 | 
			
		||||
    context
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class Kemal::ExceptionHandler
 | 
			
		||||
  exclude ["/api/v1/*"]
 | 
			
		||||
 | 
			
		||||
  private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32)
 | 
			
		||||
    return if context.response.closed?
 | 
			
		||||
    return if exclude_match? context
 | 
			
		||||
 | 
			
		||||
    if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(status_code)
 | 
			
		||||
      context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type")
 | 
			
		||||
      context.response.status_code = status_code
 | 
			
		||||
      context.response.print Kemal.config.error_handlers[status_code].call(context, exception)
 | 
			
		||||
      context
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class FilteredCompressHandler < Kemal::Handler
 | 
			
		||||
  exclude ["/videoplayback", "/videoplayback/*", "/vi/*", "/ggpht/*"]
 | 
			
		||||
 | 
			
		||||
  def call(env)
 | 
			
		||||
    return call_next env if exclude_match? env
 | 
			
		||||
 | 
			
		||||
    {% if flag?(:without_zlib) %}
 | 
			
		||||
        call_next env
 | 
			
		||||
      {% else %}
 | 
			
		||||
        request_headers = env.request.headers
 | 
			
		||||
 | 
			
		||||
        if request_headers.includes_word?("Accept-Encoding", "gzip")
 | 
			
		||||
          env.response.headers["Content-Encoding"] = "gzip"
 | 
			
		||||
          env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
 | 
			
		||||
        elsif request_headers.includes_word?("Accept-Encoding", "deflate")
 | 
			
		||||
          env.response.headers["Content-Encoding"] = "deflate"
 | 
			
		||||
          env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        call_next env
 | 
			
		||||
      {% end %}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class APIHandler < Kemal::Handler
 | 
			
		||||
  only ["/api/v1/*"]
 | 
			
		||||
 | 
			
		||||
  def call(env)
 | 
			
		||||
    return call_next env unless only_match? env
 | 
			
		||||
 | 
			
		||||
    env.response.headers["Access-Control-Allow-Origin"] = "*"
 | 
			
		||||
 | 
			
		||||
    # Here we swap out the socket IO so we can modify the response as needed
 | 
			
		||||
    output = env.response.output
 | 
			
		||||
    env.response.output = IO::Memory.new
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      call_next env
 | 
			
		||||
 | 
			
		||||
      env.response.output.rewind
 | 
			
		||||
      response = env.response.output.gets_to_end
 | 
			
		||||
 | 
			
		||||
      if env.response.headers["Content-Type"]?.try &.== "application/json"
 | 
			
		||||
        response = JSON.parse(response)
 | 
			
		||||
 | 
			
		||||
        if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
 | 
			
		||||
          response = response.to_pretty_json
 | 
			
		||||
        else
 | 
			
		||||
          response = response.to_json
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    rescue
 | 
			
		||||
    ensure
 | 
			
		||||
      env.response.output = output
 | 
			
		||||
      env.response.puts response
 | 
			
		||||
 | 
			
		||||
      env.response.flush
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class DenyFrame < Kemal::Handler
 | 
			
		||||
  exclude ["/embed/*"]
 | 
			
		||||
 | 
			
		||||
  def call(env)
 | 
			
		||||
    return call_next env if exclude_match? env
 | 
			
		||||
 | 
			
		||||
    env.response.headers["X-Frame-Options"] = "sameorigin"
 | 
			
		||||
    call_next env
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
# Temp fix for https://github.com/crystal-lang/crystal/issues/7383
 | 
			
		||||
class HTTP::Client
 | 
			
		||||
  private def handle_response(response)
 | 
			
		||||
    # close unless response.keep_alive?
 | 
			
		||||
    response
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@ -1,7 +1,5 @@
 | 
			
		||||
class Config
 | 
			
		||||
  YAML.mapping({
 | 
			
		||||
    video_threads:   Int32,      # Number of threads to use for updating videos in cache (mostly non-functional)
 | 
			
		||||
    crawl_threads:   Int32,      # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
 | 
			
		||||
    channel_threads: Int32,      # Number of threads to use for crawling videos from channels (for updating subscriptions)
 | 
			
		||||
    feed_threads:    Int32,      # Number of threads to use for updating feeds
 | 
			
		||||
    db:              NamedTuple( # Database configuration
 | 
			
		||||
@ -28,61 +26,6 @@ user: String,
 | 
			
		||||
  })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class FilteredCompressHandler < Kemal::Handler
 | 
			
		||||
  exclude ["/videoplayback", "/videoplayback/*", "/vi/*", "/api/*", "/ggpht/*"]
 | 
			
		||||
 | 
			
		||||
  def call(env)
 | 
			
		||||
    return call_next env if exclude_match? env
 | 
			
		||||
 | 
			
		||||
    {% if flag?(:without_zlib) %}
 | 
			
		||||
      call_next env
 | 
			
		||||
    {% else %}
 | 
			
		||||
      request_headers = env.request.headers
 | 
			
		||||
 | 
			
		||||
      if request_headers.includes_word?("Accept-Encoding", "gzip")
 | 
			
		||||
        env.response.headers["Content-Encoding"] = "gzip"
 | 
			
		||||
        env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
 | 
			
		||||
      elsif request_headers.includes_word?("Accept-Encoding", "deflate")
 | 
			
		||||
        env.response.headers["Content-Encoding"] = "deflate"
 | 
			
		||||
        env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      call_next env
 | 
			
		||||
    {% end %}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class APIHandler < Kemal::Handler
 | 
			
		||||
  only ["/api/v1/*"]
 | 
			
		||||
 | 
			
		||||
  def call(env)
 | 
			
		||||
    return call_next env unless only_match? env
 | 
			
		||||
 | 
			
		||||
    env.response.headers["Access-Control-Allow-Origin"] = "*"
 | 
			
		||||
 | 
			
		||||
    call_next env
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
class DenyFrame < Kemal::Handler
 | 
			
		||||
  exclude ["/embed/*"]
 | 
			
		||||
 | 
			
		||||
  def call(env)
 | 
			
		||||
    return call_next env if exclude_match? env
 | 
			
		||||
 | 
			
		||||
    env.response.headers["X-Frame-Options"] = "sameorigin"
 | 
			
		||||
    call_next env
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
# Temp fix for https://github.com/crystal-lang/crystal/issues/7383
 | 
			
		||||
class HTTP::Client
 | 
			
		||||
  private def handle_response(response)
 | 
			
		||||
    # close unless response.keep_alive?
 | 
			
		||||
    response
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def rank_videos(db, n)
 | 
			
		||||
  top = [] of {Float64, String}
 | 
			
		||||
 | 
			
		||||
@ -325,6 +268,11 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
 | 
			
		||||
        paid = true
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      premiere_timestamp = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/span[@class="localized-date"])).try &.["data-timestamp"]?.try &.to_i64
 | 
			
		||||
      if premiere_timestamp
 | 
			
		||||
        premiere_timestamp = Time.unix(premiere_timestamp)
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      items << SearchVideo.new(
 | 
			
		||||
        title: title,
 | 
			
		||||
        id: id,
 | 
			
		||||
@ -337,7 +285,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
 | 
			
		||||
        length_seconds: length_seconds,
 | 
			
		||||
        live_now: live_now,
 | 
			
		||||
        paid: paid,
 | 
			
		||||
        premium: premium
 | 
			
		||||
        premium: premium,
 | 
			
		||||
        premiere_timestamp: premiere_timestamp
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@ -1,51 +1,3 @@
 | 
			
		||||
def crawl_videos(db, logger)
 | 
			
		||||
  ids = Deque(String).new
 | 
			
		||||
  random = Random.new
 | 
			
		||||
 | 
			
		||||
  search(random.base64(3)).as(Tuple)[1].each do |video|
 | 
			
		||||
    if video.is_a?(SearchVideo)
 | 
			
		||||
      ids << video.id
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  loop do
 | 
			
		||||
    if ids.empty?
 | 
			
		||||
      search(random.base64(3)).as(Tuple)[1].each do |video|
 | 
			
		||||
        if video.is_a?(SearchVideo)
 | 
			
		||||
          ids << video.id
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    begin
 | 
			
		||||
      id = ids[0]
 | 
			
		||||
      video = get_video(id, db)
 | 
			
		||||
    rescue ex
 | 
			
		||||
      logger.write("#{id} : #{ex.message}\n")
 | 
			
		||||
      next
 | 
			
		||||
    ensure
 | 
			
		||||
      ids.delete(id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    rvs = [] of Hash(String, String)
 | 
			
		||||
    video.info["rvs"]?.try &.split(",").each do |rv|
 | 
			
		||||
      rvs << HTTP::Params.parse(rv).to_h
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    rvs.each do |rv|
 | 
			
		||||
      if rv.has_key?("id") && !db.query_one?("SELECT EXISTS (SELECT true FROM videos WHERE id = $1)", rv["id"], as: Bool)
 | 
			
		||||
        ids.delete(id)
 | 
			
		||||
        ids << rv["id"]
 | 
			
		||||
        if ids.size == 150
 | 
			
		||||
          ids.shift
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Fiber.yield
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
 | 
			
		||||
  max_channel = Channel(Int32).new
 | 
			
		||||
 | 
			
		||||
@ -82,30 +34,14 @@ def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      sleep 1.minute
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  max_channel.send(max_threads)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def refresh_videos(db, logger)
 | 
			
		||||
  loop do
 | 
			
		||||
    db.query("SELECT id FROM videos ORDER BY updated") do |rs|
 | 
			
		||||
      rs.each do
 | 
			
		||||
        begin
 | 
			
		||||
          id = rs.read(String)
 | 
			
		||||
          video = get_video(id, db)
 | 
			
		||||
        rescue ex
 | 
			
		||||
          logger.write("#{id} : #{ex.message}\n")
 | 
			
		||||
          next
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    Fiber.yield
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def refresh_feeds(db, logger, max_threads = 1)
 | 
			
		||||
  max_channel = Channel(Int32).new
 | 
			
		||||
 | 
			
		||||
@ -129,15 +65,26 @@ def refresh_feeds(db, logger, max_threads = 1)
 | 
			
		||||
          active_threads += 1
 | 
			
		||||
          spawn do
 | 
			
		||||
            begin
 | 
			
		||||
              db.query("SELECT * FROM #{view_name} LIMIT 1") do |rs|
 | 
			
		||||
                # View doesn't contain same number of rows as ChannelVideo
 | 
			
		||||
                if ChannelVideo.from_rs(rs)[0]?.try &.to_a.size.try &.!= rs.column_count
 | 
			
		||||
                  db.exec("DROP MATERIALIZED VIEW #{view_name}")
 | 
			
		||||
                  raise "valid schema does not exist"
 | 
			
		||||
                end
 | 
			
		||||
              end
 | 
			
		||||
 | 
			
		||||
              db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
 | 
			
		||||
            rescue ex
 | 
			
		||||
              # Create view if it doesn't exist
 | 
			
		||||
              if ex.message.try &.ends_with? "does not exist"
 | 
			
		||||
              if ex.message.try &.ends_with?("does not exist")
 | 
			
		||||
                # While iterating through, we may have an email stored from a deleted account
 | 
			
		||||
                if db.query_one?("SELECT true FROM users WHERE email = $1", email, as: Bool)
 | 
			
		||||
                  db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
 | 
			
		||||
                  SELECT * FROM channel_videos WHERE \
 | 
			
		||||
                  ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
 | 
			
		||||
                  ORDER BY published DESC;")
 | 
			
		||||
                logger.write("CREATE #{view_name}")
 | 
			
		||||
                  logger.write("CREATE #{view_name}\n")
 | 
			
		||||
                end
 | 
			
		||||
              else
 | 
			
		||||
                logger.write("REFRESH #{email} : #{ex.message}\n")
 | 
			
		||||
              end
 | 
			
		||||
@ -147,6 +94,8 @@ def refresh_feeds(db, logger, max_threads = 1)
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      sleep 1.minute
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@ -169,7 +118,6 @@ def subscribe_to_feeds(db, logger, key, config)
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        sleep 1.minute
 | 
			
		||||
        Fiber.yield
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
@ -200,7 +148,7 @@ def pull_top_videos(config, db)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    yield videos
 | 
			
		||||
    Fiber.yield
 | 
			
		||||
    sleep 1.minute
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -215,7 +163,7 @@ def pull_popular_videos(db)
 | 
			
		||||
    ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse
 | 
			
		||||
 | 
			
		||||
    yield videos
 | 
			
		||||
    Fiber.yield
 | 
			
		||||
    sleep 1.minute
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -228,6 +176,7 @@ def update_decrypt_function
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    yield decrypt_function
 | 
			
		||||
    sleep 1.minute
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -239,7 +188,8 @@ def find_working_proxies(regions)
 | 
			
		||||
      # proxies = filter_proxies(proxies)
 | 
			
		||||
 | 
			
		||||
      yield region, proxies
 | 
			
		||||
      Fiber.yield
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    sleep 1.minute
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ class PlaylistVideo
 | 
			
		||||
    published:      Time,
 | 
			
		||||
    playlists:      Array(String),
 | 
			
		||||
    index:          Int32,
 | 
			
		||||
    live_now:       Bool,
 | 
			
		||||
  })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -101,8 +102,10 @@ def extract_playlist(plid, nodeset, index)
 | 
			
		||||
    anchor = video.xpath_node(%q(.//td[@class="pl-video-time"]/div/div[1]))
 | 
			
		||||
    if anchor && !anchor.content.empty?
 | 
			
		||||
      length_seconds = decode_length_seconds(anchor.content)
 | 
			
		||||
      live_now = false
 | 
			
		||||
    else
 | 
			
		||||
      length_seconds = 0
 | 
			
		||||
      live_now = true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    videos << PlaylistVideo.new(
 | 
			
		||||
@ -114,6 +117,7 @@ def extract_playlist(plid, nodeset, index)
 | 
			
		||||
      published: Time.now,
 | 
			
		||||
      playlists: [plid],
 | 
			
		||||
      index: index + offset,
 | 
			
		||||
      live_now: live_now
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ class SearchVideo
 | 
			
		||||
    live_now:           Bool,
 | 
			
		||||
    paid:               Bool,
 | 
			
		||||
    premium:            Bool,
 | 
			
		||||
    premiere_timestamp: Time?,
 | 
			
		||||
  })
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -255,8 +255,12 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
 | 
			
		||||
  challenge = OpenSSL::HMAC.digest(:sha256, key, challenge)
 | 
			
		||||
  challenge = Base64.urlsafe_encode(challenge)
 | 
			
		||||
 | 
			
		||||
  if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)
 | 
			
		||||
    db.exec("DELETE FROM nonces * WHERE nonce = $1", nonce)
 | 
			
		||||
  if nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", nonce, as: {String, Time})
 | 
			
		||||
    if nonce[1] > Time.now
 | 
			
		||||
      db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.new(1990, 1, 1), nonce[0])
 | 
			
		||||
    else
 | 
			
		||||
      raise translate(locale, "Invalid token")
 | 
			
		||||
    end
 | 
			
		||||
  else
 | 
			
		||||
    raise translate(locale, "Invalid token")
 | 
			
		||||
  end
 | 
			
		||||
@ -270,7 +274,7 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if challenge_user_id != user_id
 | 
			
		||||
    raise translate(locale, "Invalid user")
 | 
			
		||||
    raise translate(locale, "Invalid token")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if expire < Time.now.to_unix
 | 
			
		||||
@ -328,7 +332,22 @@ def generate_captcha(key, db)
 | 
			
		||||
  answer = "#{hour}:#{minute.to_s.rjust(2, '0')}:#{second.to_s.rjust(2, '0')}"
 | 
			
		||||
  answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer)
 | 
			
		||||
 | 
			
		||||
  challenge, token = create_response(answer, "sign_in", key, db)
 | 
			
		||||
 | 
			
		||||
  return {image: image, challenge: challenge, token: token}
 | 
			
		||||
  return {
 | 
			
		||||
    question: image,
 | 
			
		||||
    tokens:   [create_response(answer, "sign_in", key, db)],
 | 
			
		||||
  }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
def generate_text_captcha(key, db)
 | 
			
		||||
  response = HTTP::Client.get(TEXTCAPTCHA_URL).body
 | 
			
		||||
  response = JSON.parse(response)
 | 
			
		||||
 | 
			
		||||
  tokens = response["a"].as_a.map do |answer|
 | 
			
		||||
    create_response(answer.as_s, "sign_in", key, db)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    question: response["q"].as_s,
 | 
			
		||||
    tokens:   tokens,
 | 
			
		||||
  }
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
@ -250,6 +250,63 @@ class Video
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def allow_ratings
 | 
			
		||||
    allow_ratings = player_response["videoDetails"].try &.["allowRatings"]?.try &.as_bool
 | 
			
		||||
 | 
			
		||||
    if allow_ratings.nil?
 | 
			
		||||
      return true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return allow_ratings
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def live_now
 | 
			
		||||
    live_now = self.player_response["videoDetails"]?.try &.["isLive"]?.try &.as_bool
 | 
			
		||||
 | 
			
		||||
    if live_now.nil?
 | 
			
		||||
      return false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return live_now
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def is_listed
 | 
			
		||||
    is_listed = player_response["videoDetails"].try &.["isCrawlable"]?.try &.as_bool
 | 
			
		||||
 | 
			
		||||
    if is_listed.nil?
 | 
			
		||||
      return true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return is_listed
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def is_upcoming
 | 
			
		||||
    is_upcoming = player_response["videoDetails"].try &.["isUpcoming"]?.try &.as_bool
 | 
			
		||||
 | 
			
		||||
    if is_upcoming.nil?
 | 
			
		||||
      return false
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return is_upcoming
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def premiere_timestamp
 | 
			
		||||
    if self.is_upcoming
 | 
			
		||||
      premiere_timestamp = player_response["playabilityStatus"]?
 | 
			
		||||
        .try &.["liveStreamability"]?
 | 
			
		||||
          .try &.["liveStreamabilityRenderer"]?
 | 
			
		||||
            .try &.["offlineSlate"]?
 | 
			
		||||
              .try &.["liveStreamOfflineSlateRenderer"]?
 | 
			
		||||
                .try &.["scheduledStartTime"].as_s.to_i64
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    if premiere_timestamp
 | 
			
		||||
      premiere_timestamp = Time.unix(premiere_timestamp)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return premiere_timestamp
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def keywords
 | 
			
		||||
    keywords = self.player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a
 | 
			
		||||
    keywords ||= [] of String
 | 
			
		||||
@ -644,6 +701,10 @@ def fetch_video(id, proxies, region)
 | 
			
		||||
    raise "Video unavailable."
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  if !info["title"]?
 | 
			
		||||
    raise "Video unavailable."
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  title = info["title"]
 | 
			
		||||
  author = info["author"]
 | 
			
		||||
  ucid = info["ucid"]
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,9 @@
 | 
			
		||||
            <% else %>
 | 
			
		||||
            <div class="thumbnail">
 | 
			
		||||
                <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
 | 
			
		||||
                <% if item.length_seconds != 0 %>
 | 
			
		||||
                <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
 | 
			
		||||
                <% end %>
 | 
			
		||||
            </div>
 | 
			
		||||
            <% end %>
 | 
			
		||||
            <p><%= item.title %></p>
 | 
			
		||||
@ -55,7 +57,7 @@
 | 
			
		||||
                <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
 | 
			
		||||
                <% if item.responds_to?(:live_now) && item.live_now %>
 | 
			
		||||
                <p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
 | 
			
		||||
                <% else %>
 | 
			
		||||
                <% elsif item.length_seconds != 0 %>
 | 
			
		||||
                <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
 | 
			
		||||
                <% end %>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -66,7 +68,9 @@
 | 
			
		||||
            <b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <% if Time.now - item.published > 1.minute %>
 | 
			
		||||
        <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp && item.premiere_timestamp.not_nil! > Time.now %>
 | 
			
		||||
        <h5><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.now).ago, locale)) %></h5>
 | 
			
		||||
        <% elsif Time.now - item.published > 1.minute %>
 | 
			
		||||
        <h5><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></h5>
 | 
			
		||||
        <% end %>
 | 
			
		||||
    <% else %>
 | 
			
		||||
@ -90,7 +94,7 @@
 | 
			
		||||
                <% end %>
 | 
			
		||||
                <% if item.responds_to?(:live_now) && item.live_now %>
 | 
			
		||||
                <p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
 | 
			
		||||
                <% else %>
 | 
			
		||||
                <% elsif item.length_seconds != 0 %>
 | 
			
		||||
                <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
 | 
			
		||||
                <% end %>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -101,7 +105,9 @@
 | 
			
		||||
            <b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
        <% if Time.now - item.published > 1.minute %>
 | 
			
		||||
        <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp && item.premiere_timestamp.not_nil! > Time.now %>
 | 
			
		||||
        <h5><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.now).ago, locale)) %></h5>
 | 
			
		||||
        <% elsif Time.now - item.published > 1.minute %>
 | 
			
		||||
        <h5><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></h5>
 | 
			
		||||
        <% end %>
 | 
			
		||||
    <% end %>
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,7 @@ var options = {
 | 
			
		||||
  aspectRatio: "<%= aspect_ratio %>",
 | 
			
		||||
  <% end %>
 | 
			
		||||
  preload: "auto",
 | 
			
		||||
  playbackRates: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
 | 
			
		||||
  playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
 | 
			
		||||
  controlBar: {
 | 
			
		||||
    children: [
 | 
			
		||||
      "playToggle",
 | 
			
		||||
@ -78,6 +78,7 @@ var player = videojs("player", options, function() {
 | 
			
		||||
    volumeStep: 0.1,
 | 
			
		||||
    seekStep: 5,
 | 
			
		||||
    enableModifiersForNumbers: false,
 | 
			
		||||
    enableHoverScroll: true,
 | 
			
		||||
    customKeys: {
 | 
			
		||||
      // Toggle play with K Key
 | 
			
		||||
      play: {
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="https://unpkg.com/dashjs@2.9.0/dist/dash.mediaplayer.debug.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/Dash-Industry-Forum/dash.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="/js/silvermine-videojs-quality-selector.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/omarroth/videojs-quality-selector"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="https://unpkg.com/video.js@6.12.1/dist/video.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/videojs/video.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="https://unpkg.com/videojs-contrib-quality-levels@2.0.7/dist/videojs-contrib-quality-levels.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="https://unpkg.com/videojs-contrib-dash@2.8.2/dist/videojs-dash.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/videojs/videojs-contrib-dash"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -89,7 +89,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="https://unpkg.com/@videojs/http-streaming@1.2.2/dist/videojs-http-streaming.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/videojs/http-streaming"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="https://unpkg.com/videojs-markers@1.0.1/dist/videojs-markers.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/spchuang/videojs-markers"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -117,7 +117,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="https://unpkg.com/videojs-share@2.0.1/dist/videojs-share.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/mkhazov/videojs-share"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
@ -131,7 +131,7 @@
 | 
			
		||||
            </td>
 | 
			
		||||
 | 
			
		||||
            <td>
 | 
			
		||||
                <a href="/js/videojs.hotkeys.js"><%= translate(locale, "source") %></a>
 | 
			
		||||
                <a href="https://github.com/ctd1500/videojs-hotkeys"><%= translate(locale, "source") %></a>
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
        <div class="h-box">
 | 
			
		||||
            <div class="pure-g">
 | 
			
		||||
                <div class="pure-u-1-2">
 | 
			
		||||
                    <a class="pure-button <% if account_type == "invidious" %>pure-button-disabled<% end %>" href="/login">
 | 
			
		||||
                    <a class="pure-button <% if account_type == "invidious" %>pure-button-disabled<% end %>" href="/login?type=invidious">
 | 
			
		||||
                        <%= translate(locale, "Login/Register") %>
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -22,55 +22,84 @@
 | 
			
		||||
            <% if account_type == "invidious" %>
 | 
			
		||||
            <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=invidious" method="post">
 | 
			
		||||
                <fieldset>
 | 
			
		||||
                    <% if email %>
 | 
			
		||||
                    <input name="email" type="hidden" value="<%= email %>">
 | 
			
		||||
                    <% else %>
 | 
			
		||||
                    <label for="email"><%= translate(locale, "User ID:") %></label>
 | 
			
		||||
                    <input required class="pure-input-1" name="email" type="text" placeholder="User ID">
 | 
			
		||||
                    <% end %>
 | 
			
		||||
 | 
			
		||||
                    <% if password %>
 | 
			
		||||
                    <input name="password" type="hidden" value="<%= password %>">
 | 
			
		||||
                    <% else %>
 | 
			
		||||
                    <label for="password"><%= translate(locale, "Password:") %></label>
 | 
			
		||||
                    <input required class="pure-input-1" name="password" type="password" placeholder="Password">
 | 
			
		||||
                    <% end %>
 | 
			
		||||
 | 
			
		||||
                <% if config.captcha_enabled %>
 | 
			
		||||
                    <% if captcha_type == "image" %>
 | 
			
		||||
                        <img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
 | 
			
		||||
                        <input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
 | 
			
		||||
                        <input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
 | 
			
		||||
                <% if captcha %>
 | 
			
		||||
                    <% case captcha_type when %>
 | 
			
		||||
                    <% when "image" %>
 | 
			
		||||
                        <% captcha = captcha.not_nil! %>
 | 
			
		||||
                        <img style="width:100%" src='<%= captcha[:question] %>'/>
 | 
			
		||||
                        <% captcha[:tokens].each_with_index do |token, i| %>
 | 
			
		||||
                            <input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>">
 | 
			
		||||
                            <input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>">
 | 
			
		||||
                        <% end %>
 | 
			
		||||
                        <input type="hidden" name="captcha_type" value="image">
 | 
			
		||||
                        <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
 | 
			
		||||
                        <input required type="text" name="answer" type="text" placeholder="h:mm:ss">
 | 
			
		||||
                        <input type="text" name="answer" type="text" placeholder="h:mm:ss">
 | 
			
		||||
                    <% when "text" %>
 | 
			
		||||
                        <% captcha = captcha.not_nil! %>
 | 
			
		||||
                        <% captcha[:tokens].each_with_index do |token, i| %>
 | 
			
		||||
                            <input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>">
 | 
			
		||||
                            <input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>">
 | 
			
		||||
                        <% end %>
 | 
			
		||||
                        <input type="hidden" name="captcha_type" value="text">
 | 
			
		||||
                        <label for="answer"><%= captcha[:question] %></label>
 | 
			
		||||
                        <input type="text" name="answer" type="text" placeholder="Answer">
 | 
			
		||||
                    <% end %>
 | 
			
		||||
 | 
			
		||||
                    <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
 | 
			
		||||
                        <%= translate(locale, "Register") %>
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
                    <% case captcha_type when %>
 | 
			
		||||
                    <% when "image" %>
 | 
			
		||||
                        <label>
 | 
			
		||||
                            <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">
 | 
			
		||||
                            <button type="submit" name="change_type" class="pure-button pure-button-primary" value="text">
 | 
			
		||||
                                <%= translate(locale, "Text CAPTCHA") %>
 | 
			
		||||
                            </a>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </label>
 | 
			
		||||
                    <% else %>
 | 
			
		||||
                        <% text_captcha.not_nil![:tokens].each_with_index do |token, i| %>
 | 
			
		||||
                            <input type="hidden" name="text_challenge<%= i %>" value="<%= token[0] %>">
 | 
			
		||||
                            <input type="hidden" name="text_token<%= i %>" value="<%= token[1] %>">
 | 
			
		||||
                        <% end %>
 | 
			
		||||
                        <label for="text_answer"><%= text_captcha.not_nil![:question] %></label>
 | 
			
		||||
                        <input required type="text" name="text_answer" type="text" placeholder="Answer">
 | 
			
		||||
 | 
			
		||||
                    <% when "text" %>
 | 
			
		||||
                        <label>
 | 
			
		||||
                            <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious">
 | 
			
		||||
                            <button type="submit" name="change_type" class="pure-button pure-button-primary" value="image">
 | 
			
		||||
                                <%= translate(locale, "Image CAPTCHA") %>
 | 
			
		||||
                            </a>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </label>
 | 
			
		||||
                    <% end %>
 | 
			
		||||
                <% end %>
 | 
			
		||||
 | 
			
		||||
                    <button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button>
 | 
			
		||||
                    <% if config.registration_enabled %>
 | 
			
		||||
                    <button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button>
 | 
			
		||||
                <% else %>
 | 
			
		||||
                    <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
 | 
			
		||||
                        <%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>
 | 
			
		||||
                    </button>
 | 
			
		||||
                <% end %>
 | 
			
		||||
                </fieldset>
 | 
			
		||||
            </form>
 | 
			
		||||
            <% elsif account_type == "google" %>
 | 
			
		||||
            <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>" method="post">
 | 
			
		||||
            <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=google" method="post">
 | 
			
		||||
                <fieldset>
 | 
			
		||||
                    <% if email %>
 | 
			
		||||
                    <input name="email" type="hidden" value="<%= email %>">
 | 
			
		||||
                    <% else %>
 | 
			
		||||
                    <label for="email"><%= translate(locale, "Email:") %></label>
 | 
			
		||||
                    <input required class="pure-input-1" name="email" type="email" placeholder="Email">
 | 
			
		||||
                    <% end %>
 | 
			
		||||
 | 
			
		||||
                    <% if password %>
 | 
			
		||||
                    <input name="password" type="hidden" value="<%= password %>">
 | 
			
		||||
                    <% else %>
 | 
			
		||||
                    <label for="password"><%= translate(locale, "Password:") %></label>
 | 
			
		||||
                    <input required class="pure-input-1" name="password" type="password" placeholder="Password">
 | 
			
		||||
                    <% end %>
 | 
			
		||||
 | 
			
		||||
                    <% if tfa %>
 | 
			
		||||
                    <label for="tfa"><%= translate(locale, "Google verification code:") %></label>
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ function update_value(element) {
 | 
			
		||||
            <div class="pure-control-group">
 | 
			
		||||
                <label for="speed"><%= translate(locale, "Default speed: ") %></label>
 | 
			
		||||
                <select name="speed" id="speed">
 | 
			
		||||
                <% {2.0, 1.5, 1.25, 1.0, 0.75, 0.5}.each do |option| %>
 | 
			
		||||
                <% {2.0, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
 | 
			
		||||
                    <option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
 | 
			
		||||
                <% end %>
 | 
			
		||||
                </select>
 | 
			
		||||
 | 
			
		||||
@ -181,17 +181,6 @@
 | 
			
		||||
            <% end %>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="content">
 | 
			
		||||
          <%= content %>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="footer">
 | 
			
		||||
            <div class="pure-g">
 | 
			
		||||
              <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
                <a href="https://github.com/omarroth/invidious">
 | 
			
		||||
                  <%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %>
 | 
			
		||||
                </a>
 | 
			
		||||
              </div>
 | 
			
		||||
          <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
            <i class="icon ion-logo-bitcoin"></i>
 | 
			
		||||
            BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</div>
 | 
			
		||||
@ -223,10 +212,6 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
        <div class="pure-u-1 pure-u-md-2-24"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  <script src="/js/ui.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@
 | 
			
		||||
<meta name="twitter:url" content="<%= host_url %>/watch?v=<%= video.id %>">
 | 
			
		||||
<meta name="twitter:title" content="<%= HTML.escape(video.title) %>">
 | 
			
		||||
<meta name="twitter:description" content="<%= description %>">
 | 
			
		||||
<meta name="twitter:image" content="/vi/<%= video.id %>/hqdefault.jpg">
 | 
			
		||||
<meta name="twitter:image" content="<%= host_url %>/vi/<%= video.id %>/maxres.jpg">
 | 
			
		||||
<meta name="twitter:player" content="<%= host_url %>/embed/<%= video.id %>">
 | 
			
		||||
<meta name="twitter:player:width" content="1280">
 | 
			
		||||
<meta name="twitter:player:height" content="720">
 | 
			
		||||
@ -59,17 +59,17 @@
 | 
			
		||||
                    <label for="download_widget"><%= translate(locale, "Download as: ") %></label>
 | 
			
		||||
                    <select style="width:100%" name="download_widget" id="download_widget">
 | 
			
		||||
                    <% video_streams.each do |option| %>
 | 
			
		||||
                        <option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split("/")[1].split(";")[0] %>"}'>
 | 
			
		||||
                        <option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
 | 
			
		||||
                            <%= option["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only
 | 
			
		||||
                        </option>
 | 
			
		||||
                    <% end %>
 | 
			
		||||
                    <% audio_streams.each do |option| %>
 | 
			
		||||
                        <option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split("/")[1].split(";")[0] %>"}'>
 | 
			
		||||
                        <option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
 | 
			
		||||
                            <%= option["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only
 | 
			
		||||
                        </option>
 | 
			
		||||
                    <% end %>
 | 
			
		||||
                    <% fmt_stream.each do |option| %>
 | 
			
		||||
                        <option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split("/")[1].split(";")[0] %>"}'>
 | 
			
		||||
                        <option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
 | 
			
		||||
                            <%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %>
 | 
			
		||||
                        </option>
 | 
			
		||||
                    <% end %>
 | 
			
		||||
@ -249,7 +249,7 @@ function get_playlist(timeouts = 0) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    playlist.innerHTML = ' \
 | 
			
		||||
      <h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3> \
 | 
			
		||||
      <h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3> \
 | 
			
		||||
      <hr>'
 | 
			
		||||
 | 
			
		||||
    var plid = "<%= plid %>"
 | 
			
		||||
@ -300,7 +300,7 @@ function get_playlist(timeouts = 0) {
 | 
			
		||||
 | 
			
		||||
        comments = document.getElementById("playlist");
 | 
			
		||||
        comments.innerHTML =
 | 
			
		||||
            '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3><hr>';
 | 
			
		||||
            '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3><hr>';
 | 
			
		||||
        get_playlist(timeouts + 1);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@ -319,7 +319,7 @@ function get_reddit_comments(timeouts = 0) {
 | 
			
		||||
 | 
			
		||||
  var fallback = comments.innerHTML;
 | 
			
		||||
  comments.innerHTML =
 | 
			
		||||
    '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
 | 
			
		||||
    '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
 | 
			
		||||
 | 
			
		||||
  var url = "/api/v1/comments/<%= video.id %>?source=reddit&format=html&hl=<%= env.get("preferences").as(Preferences).locale %>";
 | 
			
		||||
  var xhr = new XMLHttpRequest();
 | 
			
		||||
@ -382,7 +382,7 @@ function get_youtube_comments(timeouts = 0) {
 | 
			
		||||
 | 
			
		||||
  var fallback = comments.innerHTML;
 | 
			
		||||
  comments.innerHTML =
 | 
			
		||||
    '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
 | 
			
		||||
    '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
 | 
			
		||||
 | 
			
		||||
  var url = "/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("preferences").as(Preferences).locale %>";
 | 
			
		||||
  var xhr = new XMLHttpRequest();
 | 
			
		||||
@ -429,7 +429,7 @@ function get_youtube_comments(timeouts = 0) {
 | 
			
		||||
    console.log("Pulling comments timed out.");
 | 
			
		||||
 | 
			
		||||
    comments.innerHTML =
 | 
			
		||||
      '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
 | 
			
		||||
      '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
 | 
			
		||||
    get_youtube_comments(timeouts + 1);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -440,7 +440,7 @@ function get_youtube_replies(target, load_more) {
 | 
			
		||||
  var body = target.parentNode.parentNode;
 | 
			
		||||
  var fallback = body.innerHTML;
 | 
			
		||||
  body.innerHTML =
 | 
			
		||||
      '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
 | 
			
		||||
      '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
 | 
			
		||||
 | 
			
		||||
  var url = '/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("preferences").as(Preferences).locale %>&continuation=' +
 | 
			
		||||
      continuation;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user