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 :"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										582
									
								
								locales/pl.json
									
									
									
									
									
								
							
							
						
						
									
										582
									
								
								locales/pl.json
									
									
									
									
									
								
							@ -1,293 +1,293 @@
 | 
			
		||||
{
 | 
			
		||||
  "`x` subscribers": "`x` subskrybcji",
 | 
			
		||||
  "`x` videos": "`x` filmów",
 | 
			
		||||
  "LIVE": "NA ŻYWO",
 | 
			
		||||
  "Shared `x` ago": "Udostępniono `x` temu",
 | 
			
		||||
  "Unsubscribe": "Odsubskrybuj",
 | 
			
		||||
  "Subscribe": "Subskrybuj",
 | 
			
		||||
  "Login to subscribe to `x`": "Zaloguj się, aby subskrybować `x`",
 | 
			
		||||
  "View channel on YouTube": "Wyświetl kanał na YouTube",
 | 
			
		||||
  "newest": "najnowsze",
 | 
			
		||||
  "oldest": "najstarsze",
 | 
			
		||||
  "popular": "popularne",
 | 
			
		||||
  "last": "",
 | 
			
		||||
  "Next page": "Następna strona",
 | 
			
		||||
  "Previous page": "Poprzednia strona",
 | 
			
		||||
  "Clear watch history?": "Wyczyścić historię?",
 | 
			
		||||
  "Yes": "Tak",
 | 
			
		||||
  "No": "Nie",
 | 
			
		||||
  "Import and Export Data": "Import i eksport danych",
 | 
			
		||||
  "Import": "Import",
 | 
			
		||||
  "Import Invidious data": "Importuj dane Invidious",
 | 
			
		||||
  "Import YouTube subscriptions": "Importuj subskrybcje z YouTube",
 | 
			
		||||
  "Import FreeTube subscriptions (.db)": "Importuj subskrybcje z FreeTube (.db)",
 | 
			
		||||
  "Import NewPipe subscriptions (.json)": "Importuj subskrybcje z NewPipe (.json)",
 | 
			
		||||
  "Import NewPipe data (.zip)": "Importuj dane NewPipe (.zip)",
 | 
			
		||||
  "Export": "Eksport",
 | 
			
		||||
  "Export subscriptions as OPML": "Eksportuj subskrybcje jako OPML",
 | 
			
		||||
  "Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrybcje jako OPML (dla NewPipe i FreeTube)",
 | 
			
		||||
  "Export data as JSON": "Eksportuj dane jako JSON",
 | 
			
		||||
  "Delete account?": "Usunąć konto?",
 | 
			
		||||
  "History": "Historia",
 | 
			
		||||
  "An alternative front-end to YouTube": "Alternatywny front-end dla YouTube",
 | 
			
		||||
  "JavaScript license information": "Informacja o licencji JavaScript",
 | 
			
		||||
  "source": "źródło",
 | 
			
		||||
  "Login": "Zaloguj",
 | 
			
		||||
  "Login/Register": "Zaloguj/Zarejestruj",
 | 
			
		||||
  "Login to Google": "Zaloguj do Google",
 | 
			
		||||
  "User ID:": "ID użytkownika:",
 | 
			
		||||
  "Password:": "Hasło:",
 | 
			
		||||
  "Time (h:mm:ss):": "Godzina (h:mm:ss):",
 | 
			
		||||
  "Text CAPTCHA": "Tekst CAPTCHA",
 | 
			
		||||
  "Image CAPTCHA": "Obraz CAPTCHA",
 | 
			
		||||
  "Sign In": "Zaloguj się",
 | 
			
		||||
  "Register": "Zarejestruj się",
 | 
			
		||||
  "Email:": "Email:",
 | 
			
		||||
  "Google verification code:": "Kod weryfikacyjny Google:",
 | 
			
		||||
  "Preferences": "Preferencje",
 | 
			
		||||
  "Player preferences": "Ustawienia odtwarzacza",
 | 
			
		||||
  "Always loop: ": "Zawsze zapętlaj: ",
 | 
			
		||||
  "Autoplay: ": "Autoodtwarzanie: ",
 | 
			
		||||
  "Autoplay next video: ": "Odtwórz następny film: ",
 | 
			
		||||
  "Listen by default: ": "Tryb dźwiękowy: ",
 | 
			
		||||
  "Proxy videos? ": "",
 | 
			
		||||
  "Default speed: ": "Domyślna prędkość: ",
 | 
			
		||||
  "Preferred video quality: ": "Preferowana jakość filmów: ",
 | 
			
		||||
  "Player volume: ": "Głośność odtwarzacza: ",
 | 
			
		||||
  "Default comments: ": "Domyślne komentarze: ",
 | 
			
		||||
  "Default captions: ": "Domyślne napisy: ",
 | 
			
		||||
  "Fallback captions: ": "Zastępcze napisy: ",
 | 
			
		||||
  "Show related videos? ": "Pokaż powiązane filmy? ",
 | 
			
		||||
  "Visual preferences": "Preferencje Wizualne",
 | 
			
		||||
  "Dark mode: ": "Ciemny motyw: ",
 | 
			
		||||
  "Thin mode: ": "Tryb minimalny: ",
 | 
			
		||||
  "Subscription preferences": "Preferencje subskrybcji",
 | 
			
		||||
  "Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
 | 
			
		||||
  "Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ",
 | 
			
		||||
  "Sort videos by: ": "Sortuj filmy: ",
 | 
			
		||||
  "published": "po czasie publikacji",
 | 
			
		||||
  "published - reverse": "po czasie publikacji od najstarszych",
 | 
			
		||||
  "alphabetically": "alfabetycznie",
 | 
			
		||||
  "alphabetically - reverse": "alfabetycznie od tyłu",
 | 
			
		||||
  "channel name": "po nazwie kanału",
 | 
			
		||||
  "channel name - reverse": "po nazwie kanału od tyłu",
 | 
			
		||||
  "Only show latest video from channel: ": "Pokazuj tylko najnowszy film z kanału: ",
 | 
			
		||||
  "Only show latest unwatched video from channel: ": "Pokazuj tylko najnowszy nie obejrzany film z kanału: ",
 | 
			
		||||
  "Only show unwatched: ": "Pokazuj tylko nie obejrzane: ",
 | 
			
		||||
  "Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ",
 | 
			
		||||
  "Data preferences": "Preferencje danych",
 | 
			
		||||
  "Clear watch history": "Wyczyść historię",
 | 
			
		||||
  "Import/Export data": "Import/Eksport danych",
 | 
			
		||||
  "Manage subscriptions": "Organizuj subskrybcje",
 | 
			
		||||
  "Watch history": "Historia",
 | 
			
		||||
  "Delete account": "Usuń konto",
 | 
			
		||||
  "Administrator preferences": "Preferencje administratora",
 | 
			
		||||
  "Default homepage: ": "Domyślna strona główna: ",
 | 
			
		||||
  "Feed menu: ": "",
 | 
			
		||||
  "Top enabled? ": "",
 | 
			
		||||
  "CAPTCHA enabled? ": "CAPTCHA aktywna? ",
 | 
			
		||||
  "Login enabled? ": "Logowanie włączone? ",
 | 
			
		||||
  "Registration enabled? ": "Rejestracja włączona? ",
 | 
			
		||||
  "Report statistics? ": "Raportować statystyki? ",
 | 
			
		||||
  "Save preferences": "Zapisz preferencje",
 | 
			
		||||
  "Subscription manager": "Manager subskrybcji",
 | 
			
		||||
  "`x` subscriptions": "`x` subskrybcji",
 | 
			
		||||
  "Import/Export": "Import/Eksport",
 | 
			
		||||
  "unsubscribe": "odsubskrybuj",
 | 
			
		||||
  "Subscriptions": "Subskrybcje",
 | 
			
		||||
  "`x` unseen notifications": "`x` niewidzianych powiadomień",
 | 
			
		||||
  "search": "szukaj",
 | 
			
		||||
  "Sign out": "Wyloguj",
 | 
			
		||||
  "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.": "",
 | 
			
		||||
  "Trending": "Na czasie",
 | 
			
		||||
  "Watch video on Youtube": "Zobacz film na YouTube",
 | 
			
		||||
  "Genre: ": "Gatunek: ",
 | 
			
		||||
  "License: ": "Licencja: ",
 | 
			
		||||
  "Family friendly? ": "Przyjazny rodzinie? ",
 | 
			
		||||
  "Wilson score: ": "Punktacja Wilsona: ",
 | 
			
		||||
  "Engagement: ": "Zaangażowanie: ",
 | 
			
		||||
  "Whitelisted regions: ": "Dostępny na obszarach: ",
 | 
			
		||||
  "Blacklisted regions: ": "Niedostępny na obszarach: ",
 | 
			
		||||
  "Shared `x`": "Udostępniono `x`",
 | 
			
		||||
  "Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.",
 | 
			
		||||
  "View YouTube comments": "Wyświetl komentarze z YouTube",
 | 
			
		||||
  "View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
 | 
			
		||||
  "View `x` comments": "Wyświetl `x` komentarzy",
 | 
			
		||||
  "View Reddit comments": "Wyświetl komentarze z Redditta",
 | 
			
		||||
  "Hide replies": "Ukryj odpowiedzi",
 | 
			
		||||
  "Show replies": "Pokaż odpowiedzi",
 | 
			
		||||
  "Incorrect password": "Niepoprawne hasło",
 | 
			
		||||
  "Quota exceeded, try again in a few hours": "Przekroczony limit zapytań, spróbuj ponownie za kilka godzin",
 | 
			
		||||
  "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Nie udało się zalogować, upewnij się, że dwuetapowe uwierzytelnianie (Autentykator lub SMS) jest aktywne.",
 | 
			
		||||
  "Invalid TFA code": "Niepoprawny kod TFA",
 | 
			
		||||
  "Login failed. This may be because two-factor authentication is not enabled on your account.": "Nie udało się zalogować. To może być spowodowane wyłączoną dwustopniową autoryzacją na twoim koncie.",
 | 
			
		||||
  "Invalid answer": "Niepoprawna odpowiedź",
 | 
			
		||||
  "Invalid CAPTCHA": "CAPTCHA wykonane błędnie",
 | 
			
		||||
  "CAPTCHA is a required field": "CAPTCHA jest polem wymaganym",
 | 
			
		||||
  "User ID is a required field": "ID użytkownika jest polem wymaganym",
 | 
			
		||||
  "Password is a required field": "Hasło jest polem wymaganym",
 | 
			
		||||
  "Invalid username or password": "Niepoprawny login lub hasło",
 | 
			
		||||
  "Please sign in using 'Sign in with Google'": "Zaloguj się używając \"Zaloguj się przez Google\"",
 | 
			
		||||
  "Password cannot be empty": "Hasło nie może być puste",
 | 
			
		||||
  "Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków",
 | 
			
		||||
  "Please sign in": "Proszę się zalogować",
 | 
			
		||||
  "Invidious Private Feed for `x`": "",
 | 
			
		||||
  "channel:`x`": "kanał:`x",
 | 
			
		||||
  "Deleted or invalid channel": "Usunięty lub niepoprawny kanał",
 | 
			
		||||
  "This channel does not exist.": "Ten kanał nie istnieje.",
 | 
			
		||||
  "Could not get channel info.": "Nie udało się uzyskać informacji o kanale.",
 | 
			
		||||
  "Could not fetch comments": "Nie udało się pobrać komentarzy",
 | 
			
		||||
  "View `x` replies": "Wyświetl `x` odpowiedzi",
 | 
			
		||||
  "`x` ago": "`x` temu",
 | 
			
		||||
  "Load more": "Wczytaj więcej",
 | 
			
		||||
  "`x` points": "`x` punktów",
 | 
			
		||||
  "Could not create mix.": "Nie udało się utworzyć miksu.",
 | 
			
		||||
  "Playlist is empty": "Lista odtwarzania jest pusta",
 | 
			
		||||
  "Invalid playlist.": "Niepoprawna lista.",
 | 
			
		||||
  "Playlist does not exist.": "Lista odtwarzania nie istnieje.",
 | 
			
		||||
  "Could not pull trending pages.": "Nie udało się pobrać strony na czasie.",
 | 
			
		||||
  "Hidden field \"challenge\" is a required field": "Ukryte pole \"wyzwanie\" jest polem wymaganym",
 | 
			
		||||
  "Hidden field \"token\" is a required field": "Ukryte pole \"token\" jest polem wymaganym",
 | 
			
		||||
  "Invalid challenge": "Niepoprawne wyzwanie",
 | 
			
		||||
  "Invalid token": "Niepoprawny token",
 | 
			
		||||
  "Invalid user": "Niepoprawny użytkownik",
 | 
			
		||||
  "Token is expired, please try again": "Token wygasł, spróbuj ponownie",
 | 
			
		||||
  "English": "angielski",
 | 
			
		||||
  "English (auto-generated)": "angielski (automatycznie generowane)",
 | 
			
		||||
  "Afrikaans": "afrykanerski",
 | 
			
		||||
  "Albanian": "albański",
 | 
			
		||||
  "Amharic": "amharski",
 | 
			
		||||
  "Arabic": "arabski",
 | 
			
		||||
  "Armenian": "armeński",
 | 
			
		||||
  "Azerbaijani": "azerski",
 | 
			
		||||
  "Bangla": "bengalski",
 | 
			
		||||
  "Basque": "baskijski",
 | 
			
		||||
  "Belarusian": "białoruski",
 | 
			
		||||
  "Bosnian": "bośniacki",
 | 
			
		||||
  "Bulgarian": "bułgarski",
 | 
			
		||||
  "Burmese": "birmański",
 | 
			
		||||
  "Catalan": "kataloński",
 | 
			
		||||
  "Cebuano": "cebuański",
 | 
			
		||||
  "Chinese (Simplified)": "chiński (uproszczony)",
 | 
			
		||||
  "Chinese (Traditional)": "chiński (tradycyjny)",
 | 
			
		||||
  "Corsican": "korsykański",
 | 
			
		||||
  "Croatian": "chorwacki",
 | 
			
		||||
  "Czech": "czeski",
 | 
			
		||||
  "Danish": "duński",
 | 
			
		||||
  "Dutch": "holenderski",
 | 
			
		||||
  "Esperanto": "esperanto",
 | 
			
		||||
  "Estonian": "estoński",
 | 
			
		||||
  "Filipino": "filipiński",
 | 
			
		||||
  "Finnish": "fiński",
 | 
			
		||||
  "French": "francuski",
 | 
			
		||||
  "Galician": "galicyjski",
 | 
			
		||||
  "Georgian": "gruziński",
 | 
			
		||||
  "German": "niemiecki",
 | 
			
		||||
  "Greek": "grecki",
 | 
			
		||||
  "Gujarati": "gudźarati",
 | 
			
		||||
  "Haitian Creole": "kreolski haitański",
 | 
			
		||||
  "Hausa": "hausa",
 | 
			
		||||
  "Hawaiian": "hawajski",
 | 
			
		||||
  "Hebrew": "hebrajski",
 | 
			
		||||
  "Hindi": "hindi",
 | 
			
		||||
  "Hmong": "hmong",
 | 
			
		||||
  "Hungarian": "węgierski",
 | 
			
		||||
  "Icelandic": "islandzki",
 | 
			
		||||
  "Igbo": "ibo",
 | 
			
		||||
  "Indonesian": "indonezyjski",
 | 
			
		||||
  "Irish": "irlandzki",
 | 
			
		||||
  "Italian": "włoski",
 | 
			
		||||
  "Japanese": "japoński",
 | 
			
		||||
  "Javanese": "jawajski",
 | 
			
		||||
  "Kannada": "kannada",
 | 
			
		||||
  "Kazakh": "kazachski",
 | 
			
		||||
  "Khmer": "khmerski",
 | 
			
		||||
  "Korean": "koreański",
 | 
			
		||||
  "Kurdish": "kurdyjski",
 | 
			
		||||
  "Kyrgyz": "kirgiski",
 | 
			
		||||
  "Lao": "laotański",
 | 
			
		||||
  "Latin": "łaciński",
 | 
			
		||||
  "Latvian": "łotewski",
 | 
			
		||||
  "Lithuanian": "litewski",
 | 
			
		||||
  "Luxembourgish": "luksemburski",
 | 
			
		||||
  "Macedonian": "macedoński",
 | 
			
		||||
  "Malagasy": "malgaski",
 | 
			
		||||
  "Malay": "malajski",
 | 
			
		||||
  "Malayalam": "malajalam",
 | 
			
		||||
  "Maltese": "maltański",
 | 
			
		||||
  "Maori": "maoryski",
 | 
			
		||||
  "Marathi": "marathi",
 | 
			
		||||
  "Mongolian": "mongolski",
 | 
			
		||||
  "Nepali": "nepalski",
 | 
			
		||||
  "Norwegian": "norweski",
 | 
			
		||||
  "Nyanja": "njandża",
 | 
			
		||||
  "Pashto": "paszto",
 | 
			
		||||
  "Persian": "perski",
 | 
			
		||||
  "Polish": "polski",
 | 
			
		||||
  "Portuguese": "portugalski",
 | 
			
		||||
  "Punjabi": "pendżabski",
 | 
			
		||||
  "Romanian": "rumuński",
 | 
			
		||||
  "Russian": "rosyjski",
 | 
			
		||||
  "Samoan": "samoański",
 | 
			
		||||
  "Scottish Gaelic": "gaelicki szkocki",
 | 
			
		||||
  "Serbian": "serbski",
 | 
			
		||||
  "Shona": "shona",
 | 
			
		||||
  "Sindhi": "sindhi",
 | 
			
		||||
  "Sinhala": "syngaleski",
 | 
			
		||||
  "Slovak": "słowacki",
 | 
			
		||||
  "Slovenian": "słoweński",
 | 
			
		||||
  "Somali": "somalijski",
 | 
			
		||||
  "Southern Sotho": "sotho południowy",
 | 
			
		||||
  "Spanish": "hiszpański",
 | 
			
		||||
  "Spanish (Latin America)": "hiszpański (ameryka łacińska)",
 | 
			
		||||
  "Sundanese": "sundajski",
 | 
			
		||||
  "Swahili": "suahili",
 | 
			
		||||
  "Swedish": "szwedzki",
 | 
			
		||||
  "Tajik": "tadżycki",
 | 
			
		||||
  "Tamil": "tamilski",
 | 
			
		||||
  "Telugu": "telugu",
 | 
			
		||||
  "Thai": "tajski",
 | 
			
		||||
  "Turkish": "turecki",
 | 
			
		||||
  "Ukrainian": "ukraiński",
 | 
			
		||||
  "Urdu": "urdu",
 | 
			
		||||
  "Uzbek": "uzbecki",
 | 
			
		||||
  "Vietnamese": "wietnamski",
 | 
			
		||||
  "Welsh": "walijski",
 | 
			
		||||
  "Western Frisian": "zachodniofryzyjski",
 | 
			
		||||
  "Xhosa": "xhosa",
 | 
			
		||||
  "Yiddish": "jidysz",
 | 
			
		||||
  "Yoruba": "joruba",
 | 
			
		||||
  "Zulu": "zuluski",
 | 
			
		||||
  "`x` years": "`x` lat",
 | 
			
		||||
  "`x` months": "`x` miesięcy",
 | 
			
		||||
  "`x` weeks": "`x` tygodni",
 | 
			
		||||
  "`x` days": "`x` dni",
 | 
			
		||||
  "`x` hours": "`x` godzin",
 | 
			
		||||
  "`x` minutes": "`x` minut",
 | 
			
		||||
  "`x` seconds": "`x` sekund",
 | 
			
		||||
  "Fallback comments: ": "Zastępcze komentarze: ",
 | 
			
		||||
  "Popular": "Popularne",
 | 
			
		||||
  "Top": "Na czasie",
 | 
			
		||||
  "About": "Informacje",
 | 
			
		||||
  "Rating: ": "Ocena: ",
 | 
			
		||||
  "Language: ": "Język: ",
 | 
			
		||||
  "Default": "Domyślnie",
 | 
			
		||||
  "Music": "Muzyka",
 | 
			
		||||
  "Gaming": "Gry",
 | 
			
		||||
  "News": "Wiadomości",
 | 
			
		||||
  "Movies": "Filmy",
 | 
			
		||||
  "Download": "Pobierz",
 | 
			
		||||
  "Download as: ": "Pobierz jako: ",
 | 
			
		||||
  "%A %B %-d, %Y": "",
 | 
			
		||||
  "(edited)": "(edytowany)",
 | 
			
		||||
  "Youtube permalink of the comment": "Odnośnik bezpośredni do komentarza na YouTube",
 | 
			
		||||
  "`x` marked it with a ❤": "'x' oznaczonych ❤",
 | 
			
		||||
  "Audio mode": "Tryb audio",
 | 
			
		||||
  "Video mode": "Tryb wideo",
 | 
			
		||||
  "Videos": "Filmy",
 | 
			
		||||
  "Playlists": "Playlisty",
 | 
			
		||||
  "Current version: ": "Aktualna wersja: "
 | 
			
		||||
    "`x` subscribers": "`x` subskrybcji",
 | 
			
		||||
    "`x` videos": "`x` filmów",
 | 
			
		||||
    "LIVE": "NA ŻYWO",
 | 
			
		||||
    "Shared `x` ago": "Udostępniono `x` temu",
 | 
			
		||||
    "Unsubscribe": "Odsubskrybuj",
 | 
			
		||||
    "Subscribe": "Subskrybuj",
 | 
			
		||||
    "Login to subscribe to `x`": "Zaloguj się, aby subskrybować `x`",
 | 
			
		||||
    "View channel on YouTube": "Wyświetl kanał na YouTube",
 | 
			
		||||
    "newest": "najnowsze",
 | 
			
		||||
    "oldest": "najstarsze",
 | 
			
		||||
    "popular": "popularne",
 | 
			
		||||
    "last": "ostatnie",
 | 
			
		||||
    "Next page": "Następna strona",
 | 
			
		||||
    "Previous page": "Poprzednia strona",
 | 
			
		||||
    "Clear watch history?": "Wyczyścić historię?",
 | 
			
		||||
    "Yes": "Tak",
 | 
			
		||||
    "No": "Nie",
 | 
			
		||||
    "Import and Export Data": "Import i eksport danych",
 | 
			
		||||
    "Import": "Import",
 | 
			
		||||
    "Import Invidious data": "Importuj dane Invidious",
 | 
			
		||||
    "Import YouTube subscriptions": "Importuj subskrybcje z YouTube",
 | 
			
		||||
    "Import FreeTube subscriptions (.db)": "Importuj subskrybcje z FreeTube (.db)",
 | 
			
		||||
    "Import NewPipe subscriptions (.json)": "Importuj subskrybcje z NewPipe (.json)",
 | 
			
		||||
    "Import NewPipe data (.zip)": "Importuj dane NewPipe (.zip)",
 | 
			
		||||
    "Export": "Eksport",
 | 
			
		||||
    "Export subscriptions as OPML": "Eksportuj subskrybcje jako OPML",
 | 
			
		||||
    "Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrybcje jako OPML (dla NewPipe i FreeTube)",
 | 
			
		||||
    "Export data as JSON": "Eksportuj dane jako JSON",
 | 
			
		||||
    "Delete account?": "Usunąć konto?",
 | 
			
		||||
    "History": "Historia",
 | 
			
		||||
    "An alternative front-end to YouTube": "Alternatywny front-end dla YouTube",
 | 
			
		||||
    "JavaScript license information": "Informacja o licencji JavaScript",
 | 
			
		||||
    "source": "źródło",
 | 
			
		||||
    "Login": "Zaloguj",
 | 
			
		||||
    "Login/Register": "Zaloguj/Zarejestruj",
 | 
			
		||||
    "Login to Google": "Zaloguj do Google",
 | 
			
		||||
    "User ID:": "ID użytkownika:",
 | 
			
		||||
    "Password:": "Hasło:",
 | 
			
		||||
    "Time (h:mm:ss):": "Godzina (h:mm:ss):",
 | 
			
		||||
    "Text CAPTCHA": "Tekst CAPTCHA",
 | 
			
		||||
    "Image CAPTCHA": "Obraz CAPTCHA",
 | 
			
		||||
    "Sign In": "Zaloguj się",
 | 
			
		||||
    "Register": "Zarejestruj się",
 | 
			
		||||
    "Email:": "Email:",
 | 
			
		||||
    "Google verification code:": "Kod weryfikacyjny Google:",
 | 
			
		||||
    "Preferences": "Preferencje",
 | 
			
		||||
    "Player preferences": "Ustawienia odtwarzacza",
 | 
			
		||||
    "Always loop: ": "Zawsze zapętlaj: ",
 | 
			
		||||
    "Autoplay: ": "Autoodtwarzanie: ",
 | 
			
		||||
    "Autoplay next video: ": "Odtwórz następny film: ",
 | 
			
		||||
    "Listen by default: ": "Tryb dźwiękowy: ",
 | 
			
		||||
    "Proxy videos? ": "Filmy przez proxy? ",
 | 
			
		||||
    "Default speed: ": "Domyślna prędkość: ",
 | 
			
		||||
    "Preferred video quality: ": "Preferowana jakość filmów: ",
 | 
			
		||||
    "Player volume: ": "Głośność odtwarzacza: ",
 | 
			
		||||
    "Default comments: ": "Domyślne komentarze: ",
 | 
			
		||||
    "Default captions: ": "Domyślne napisy: ",
 | 
			
		||||
    "Fallback captions: ": "Zastępcze napisy: ",
 | 
			
		||||
    "Show related videos? ": "Pokaż powiązane filmy? ",
 | 
			
		||||
    "Visual preferences": "Preferencje Wizualne",
 | 
			
		||||
    "Dark mode: ": "Ciemny motyw: ",
 | 
			
		||||
    "Thin mode: ": "Tryb minimalny: ",
 | 
			
		||||
    "Subscription preferences": "Preferencje subskrybcji",
 | 
			
		||||
    "Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
 | 
			
		||||
    "Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ",
 | 
			
		||||
    "Sort videos by: ": "Sortuj filmy: ",
 | 
			
		||||
    "published": "po czasie publikacji",
 | 
			
		||||
    "published - reverse": "po czasie publikacji od najstarszych",
 | 
			
		||||
    "alphabetically": "alfabetycznie",
 | 
			
		||||
    "alphabetically - reverse": "alfabetycznie od tyłu",
 | 
			
		||||
    "channel name": "po nazwie kanału",
 | 
			
		||||
    "channel name - reverse": "po nazwie kanału od tyłu",
 | 
			
		||||
    "Only show latest video from channel: ": "Pokazuj tylko najnowszy film z kanału: ",
 | 
			
		||||
    "Only show latest unwatched video from channel: ": "Pokazuj tylko najnowszy nie obejrzany film z kanału: ",
 | 
			
		||||
    "Only show unwatched: ": "Pokazuj tylko nie obejrzane: ",
 | 
			
		||||
    "Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ",
 | 
			
		||||
    "Data preferences": "Preferencje danych",
 | 
			
		||||
    "Clear watch history": "Wyczyść historię",
 | 
			
		||||
    "Import/Export data": "Import/Eksport danych",
 | 
			
		||||
    "Manage subscriptions": "Organizuj subskrybcje",
 | 
			
		||||
    "Watch history": "Historia",
 | 
			
		||||
    "Delete account": "Usuń konto",
 | 
			
		||||
    "Administrator preferences": "Preferencje administratora",
 | 
			
		||||
    "Default homepage: ": "Domyślna strona główna: ",
 | 
			
		||||
    "Feed menu: ": "",
 | 
			
		||||
    "Top enabled? ": "",
 | 
			
		||||
    "CAPTCHA enabled? ": "CAPTCHA aktywna? ",
 | 
			
		||||
    "Login enabled? ": "Logowanie włączone? ",
 | 
			
		||||
    "Registration enabled? ": "Rejestracja włączona? ",
 | 
			
		||||
    "Report statistics? ": "Raportować statystyki? ",
 | 
			
		||||
    "Save preferences": "Zapisz preferencje",
 | 
			
		||||
    "Subscription manager": "Manager subskrybcji",
 | 
			
		||||
    "`x` subscriptions": "`x` subskrybcji",
 | 
			
		||||
    "Import/Export": "Import/Eksport",
 | 
			
		||||
    "unsubscribe": "odsubskrybuj",
 | 
			
		||||
    "Subscriptions": "Subskrybcje",
 | 
			
		||||
    "`x` unseen notifications": "`x` niewidzianych powiadomień",
 | 
			
		||||
    "search": "szukaj",
 | 
			
		||||
    "Sign out": "Wyloguj",
 | 
			
		||||
    "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.": "Polityka prywatności.",
 | 
			
		||||
    "Trending": "Na czasie",
 | 
			
		||||
    "Watch video on Youtube": "Zobacz film na YouTube",
 | 
			
		||||
    "Genre: ": "Gatunek: ",
 | 
			
		||||
    "License: ": "Licencja: ",
 | 
			
		||||
    "Family friendly? ": "Przyjazny rodzinie? ",
 | 
			
		||||
    "Wilson score: ": "Punktacja Wilsona: ",
 | 
			
		||||
    "Engagement: ": "Zaangażowanie: ",
 | 
			
		||||
    "Whitelisted regions: ": "Dostępny na obszarach: ",
 | 
			
		||||
    "Blacklisted regions: ": "Niedostępny na obszarach: ",
 | 
			
		||||
    "Shared `x`": "Udostępniono `x`",
 | 
			
		||||
    "Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.",
 | 
			
		||||
    "View YouTube comments": "Wyświetl komentarze z YouTube",
 | 
			
		||||
    "View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
 | 
			
		||||
    "View `x` comments": "Wyświetl `x` komentarzy",
 | 
			
		||||
    "View Reddit comments": "Wyświetl komentarze z Redditta",
 | 
			
		||||
    "Hide replies": "Ukryj odpowiedzi",
 | 
			
		||||
    "Show replies": "Pokaż odpowiedzi",
 | 
			
		||||
    "Incorrect password": "Niepoprawne hasło",
 | 
			
		||||
    "Quota exceeded, try again in a few hours": "Przekroczony limit zapytań, spróbuj ponownie za kilka godzin",
 | 
			
		||||
    "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Nie udało się zalogować, upewnij się, że dwuetapowe uwierzytelnianie (Autentykator lub SMS) jest aktywne.",
 | 
			
		||||
    "Invalid TFA code": "Niepoprawny kod TFA",
 | 
			
		||||
    "Login failed. This may be because two-factor authentication is not enabled on your account.": "Nie udało się zalogować. To może być spowodowane wyłączoną dwustopniową autoryzacją na twoim koncie.",
 | 
			
		||||
    "Invalid answer": "Niepoprawna odpowiedź",
 | 
			
		||||
    "Invalid CAPTCHA": "CAPTCHA wykonane błędnie",
 | 
			
		||||
    "CAPTCHA is a required field": "CAPTCHA jest polem wymaganym",
 | 
			
		||||
    "User ID is a required field": "ID użytkownika jest polem wymaganym",
 | 
			
		||||
    "Password is a required field": "Hasło jest polem wymaganym",
 | 
			
		||||
    "Invalid username or password": "Niepoprawny login lub hasło",
 | 
			
		||||
    "Please sign in using 'Sign in with Google'": "Zaloguj się używając \"Zaloguj się przez Google\"",
 | 
			
		||||
    "Password cannot be empty": "Hasło nie może być puste",
 | 
			
		||||
    "Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków",
 | 
			
		||||
    "Please sign in": "Proszę się zalogować",
 | 
			
		||||
    "Invidious Private Feed for `x`": "",
 | 
			
		||||
    "channel:`x`": "kanał:`x",
 | 
			
		||||
    "Deleted or invalid channel": "Usunięty lub niepoprawny kanał",
 | 
			
		||||
    "This channel does not exist.": "Ten kanał nie istnieje.",
 | 
			
		||||
    "Could not get channel info.": "Nie udało się uzyskać informacji o kanale.",
 | 
			
		||||
    "Could not fetch comments": "Nie udało się pobrać komentarzy",
 | 
			
		||||
    "View `x` replies": "Wyświetl `x` odpowiedzi",
 | 
			
		||||
    "`x` ago": "`x` temu",
 | 
			
		||||
    "Load more": "Wczytaj więcej",
 | 
			
		||||
    "`x` points": "`x` punktów",
 | 
			
		||||
    "Could not create mix.": "Nie udało się utworzyć miksu.",
 | 
			
		||||
    "Playlist is empty": "Lista odtwarzania jest pusta",
 | 
			
		||||
    "Invalid playlist.": "Niepoprawna lista.",
 | 
			
		||||
    "Playlist does not exist.": "Lista odtwarzania nie istnieje.",
 | 
			
		||||
    "Could not pull trending pages.": "Nie udało się pobrać strony na czasie.",
 | 
			
		||||
    "Hidden field \"challenge\" is a required field": "Ukryte pole \"wyzwanie\" jest polem wymaganym",
 | 
			
		||||
    "Hidden field \"token\" is a required field": "Ukryte pole \"token\" jest polem wymaganym",
 | 
			
		||||
    "Invalid challenge": "Niepoprawne wyzwanie",
 | 
			
		||||
    "Invalid token": "Niepoprawny token",
 | 
			
		||||
    "Invalid user": "Niepoprawny użytkownik",
 | 
			
		||||
    "Token is expired, please try again": "Token wygasł, spróbuj ponownie",
 | 
			
		||||
    "English": "angielski",
 | 
			
		||||
    "English (auto-generated)": "angielski (automatycznie generowane)",
 | 
			
		||||
    "Afrikaans": "afrykanerski",
 | 
			
		||||
    "Albanian": "albański",
 | 
			
		||||
    "Amharic": "amharski",
 | 
			
		||||
    "Arabic": "arabski",
 | 
			
		||||
    "Armenian": "armeński",
 | 
			
		||||
    "Azerbaijani": "azerski",
 | 
			
		||||
    "Bangla": "bengalski",
 | 
			
		||||
    "Basque": "baskijski",
 | 
			
		||||
    "Belarusian": "białoruski",
 | 
			
		||||
    "Bosnian": "bośniacki",
 | 
			
		||||
    "Bulgarian": "bułgarski",
 | 
			
		||||
    "Burmese": "birmański",
 | 
			
		||||
    "Catalan": "kataloński",
 | 
			
		||||
    "Cebuano": "cebuański",
 | 
			
		||||
    "Chinese (Simplified)": "chiński (uproszczony)",
 | 
			
		||||
    "Chinese (Traditional)": "chiński (tradycyjny)",
 | 
			
		||||
    "Corsican": "korsykański",
 | 
			
		||||
    "Croatian": "chorwacki",
 | 
			
		||||
    "Czech": "czeski",
 | 
			
		||||
    "Danish": "duński",
 | 
			
		||||
    "Dutch": "holenderski",
 | 
			
		||||
    "Esperanto": "esperanto",
 | 
			
		||||
    "Estonian": "estoński",
 | 
			
		||||
    "Filipino": "filipiński",
 | 
			
		||||
    "Finnish": "fiński",
 | 
			
		||||
    "French": "francuski",
 | 
			
		||||
    "Galician": "galicyjski",
 | 
			
		||||
    "Georgian": "gruziński",
 | 
			
		||||
    "German": "niemiecki",
 | 
			
		||||
    "Greek": "grecki",
 | 
			
		||||
    "Gujarati": "gudźarati",
 | 
			
		||||
    "Haitian Creole": "kreolski haitański",
 | 
			
		||||
    "Hausa": "hausa",
 | 
			
		||||
    "Hawaiian": "hawajski",
 | 
			
		||||
    "Hebrew": "hebrajski",
 | 
			
		||||
    "Hindi": "hindi",
 | 
			
		||||
    "Hmong": "hmong",
 | 
			
		||||
    "Hungarian": "węgierski",
 | 
			
		||||
    "Icelandic": "islandzki",
 | 
			
		||||
    "Igbo": "ibo",
 | 
			
		||||
    "Indonesian": "indonezyjski",
 | 
			
		||||
    "Irish": "irlandzki",
 | 
			
		||||
    "Italian": "włoski",
 | 
			
		||||
    "Japanese": "japoński",
 | 
			
		||||
    "Javanese": "jawajski",
 | 
			
		||||
    "Kannada": "kannada",
 | 
			
		||||
    "Kazakh": "kazachski",
 | 
			
		||||
    "Khmer": "khmerski",
 | 
			
		||||
    "Korean": "koreański",
 | 
			
		||||
    "Kurdish": "kurdyjski",
 | 
			
		||||
    "Kyrgyz": "kirgiski",
 | 
			
		||||
    "Lao": "laotański",
 | 
			
		||||
    "Latin": "łaciński",
 | 
			
		||||
    "Latvian": "łotewski",
 | 
			
		||||
    "Lithuanian": "litewski",
 | 
			
		||||
    "Luxembourgish": "luksemburski",
 | 
			
		||||
    "Macedonian": "macedoński",
 | 
			
		||||
    "Malagasy": "malgaski",
 | 
			
		||||
    "Malay": "malajski",
 | 
			
		||||
    "Malayalam": "malajalam",
 | 
			
		||||
    "Maltese": "maltański",
 | 
			
		||||
    "Maori": "maoryski",
 | 
			
		||||
    "Marathi": "marathi",
 | 
			
		||||
    "Mongolian": "mongolski",
 | 
			
		||||
    "Nepali": "nepalski",
 | 
			
		||||
    "Norwegian": "norweski",
 | 
			
		||||
    "Nyanja": "njandża",
 | 
			
		||||
    "Pashto": "paszto",
 | 
			
		||||
    "Persian": "perski",
 | 
			
		||||
    "Polish": "polski",
 | 
			
		||||
    "Portuguese": "portugalski",
 | 
			
		||||
    "Punjabi": "pendżabski",
 | 
			
		||||
    "Romanian": "rumuński",
 | 
			
		||||
    "Russian": "rosyjski",
 | 
			
		||||
    "Samoan": "samoański",
 | 
			
		||||
    "Scottish Gaelic": "gaelicki szkocki",
 | 
			
		||||
    "Serbian": "serbski",
 | 
			
		||||
    "Shona": "shona",
 | 
			
		||||
    "Sindhi": "sindhi",
 | 
			
		||||
    "Sinhala": "syngaleski",
 | 
			
		||||
    "Slovak": "słowacki",
 | 
			
		||||
    "Slovenian": "słoweński",
 | 
			
		||||
    "Somali": "somalijski",
 | 
			
		||||
    "Southern Sotho": "sotho południowy",
 | 
			
		||||
    "Spanish": "hiszpański",
 | 
			
		||||
    "Spanish (Latin America)": "hiszpański (ameryka łacińska)",
 | 
			
		||||
    "Sundanese": "sundajski",
 | 
			
		||||
    "Swahili": "suahili",
 | 
			
		||||
    "Swedish": "szwedzki",
 | 
			
		||||
    "Tajik": "tadżycki",
 | 
			
		||||
    "Tamil": "tamilski",
 | 
			
		||||
    "Telugu": "telugu",
 | 
			
		||||
    "Thai": "tajski",
 | 
			
		||||
    "Turkish": "turecki",
 | 
			
		||||
    "Ukrainian": "ukraiński",
 | 
			
		||||
    "Urdu": "urdu",
 | 
			
		||||
    "Uzbek": "uzbecki",
 | 
			
		||||
    "Vietnamese": "wietnamski",
 | 
			
		||||
    "Welsh": "walijski",
 | 
			
		||||
    "Western Frisian": "zachodniofryzyjski",
 | 
			
		||||
    "Xhosa": "xhosa",
 | 
			
		||||
    "Yiddish": "jidysz",
 | 
			
		||||
    "Yoruba": "joruba",
 | 
			
		||||
    "Zulu": "zuluski",
 | 
			
		||||
    "`x` years": "`x` lat",
 | 
			
		||||
    "`x` months": "`x` miesięcy",
 | 
			
		||||
    "`x` weeks": "`x` tygodni",
 | 
			
		||||
    "`x` days": "`x` dni",
 | 
			
		||||
    "`x` hours": "`x` godzin",
 | 
			
		||||
    "`x` minutes": "`x` minut",
 | 
			
		||||
    "`x` seconds": "`x` sekund",
 | 
			
		||||
    "Fallback comments: ": "Zastępcze komentarze: ",
 | 
			
		||||
    "Popular": "Popularne",
 | 
			
		||||
    "Top": "Najczęściej oglądane",
 | 
			
		||||
    "About": "Informacje",
 | 
			
		||||
    "Rating: ": "Ocena: ",
 | 
			
		||||
    "Language: ": "Język: ",
 | 
			
		||||
    "Default": "Domyślnie",
 | 
			
		||||
    "Music": "Muzyka",
 | 
			
		||||
    "Gaming": "Gry",
 | 
			
		||||
    "News": "Wiadomości",
 | 
			
		||||
    "Movies": "Filmy",
 | 
			
		||||
    "Download": "Pobierz",
 | 
			
		||||
    "Download as: ": "Pobierz jako: ",
 | 
			
		||||
    "%A %B %-d, %Y": "",
 | 
			
		||||
    "(edited)": "(edytowany)",
 | 
			
		||||
    "Youtube permalink of the comment": "Odnośnik bezpośredni do komentarza na YouTube",
 | 
			
		||||
    "`x` marked it with a ❤": "'x' oznaczonych ❤",
 | 
			
		||||
    "Audio mode": "Tryb audio",
 | 
			
		||||
    "Video mode": "Tryb wideo",
 | 
			
		||||
    "Videos": "Filmy",
 | 
			
		||||
    "Playlists": "Playlisty",
 | 
			
		||||
    "Current version: ": "Aktualna wersja: "
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ name: invidious
 | 
			
		||||
version: 0.15.0
 | 
			
		||||
 | 
			
		||||
authors:
 | 
			
		||||
  - Omar Roth <omarroth@hotmail.com>
 | 
			
		||||
  - Omar Roth <omarroth@protonmail.com>
 | 
			
		||||
 | 
			
		||||
targets:
 | 
			
		||||
  invidious:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										508
									
								
								src/invidious.cr
									
									
									
									
									
								
							
							
						
						
									
										508
									
								
								src/invidious.cr
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -10,13 +10,15 @@ end
 | 
			
		||||
 | 
			
		||||
class ChannelVideo
 | 
			
		||||
  add_mapping({
 | 
			
		||||
    id:             String,
 | 
			
		||||
    title:          String,
 | 
			
		||||
    published:      Time,
 | 
			
		||||
    updated:        Time,
 | 
			
		||||
    ucid:           String,
 | 
			
		||||
    author:         String,
 | 
			
		||||
    length_seconds: {type: Int32, default: 0},
 | 
			
		||||
    id:                 String,
 | 
			
		||||
    title:              String,
 | 
			
		||||
    published:          Time,
 | 
			
		||||
    updated:            Time,
 | 
			
		||||
    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"
 | 
			
		||||
                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}")
 | 
			
		||||
              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}\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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,18 @@
 | 
			
		||||
class SearchVideo
 | 
			
		||||
  add_mapping({
 | 
			
		||||
    title:            String,
 | 
			
		||||
    id:               String,
 | 
			
		||||
    author:           String,
 | 
			
		||||
    ucid:             String,
 | 
			
		||||
    published:        Time,
 | 
			
		||||
    views:            Int64,
 | 
			
		||||
    description:      String,
 | 
			
		||||
    description_html: String,
 | 
			
		||||
    length_seconds:   Int32,
 | 
			
		||||
    live_now:         Bool,
 | 
			
		||||
    paid:             Bool,
 | 
			
		||||
    premium:          Bool,
 | 
			
		||||
    title:              String,
 | 
			
		||||
    id:                 String,
 | 
			
		||||
    author:             String,
 | 
			
		||||
    ucid:               String,
 | 
			
		||||
    published:          Time,
 | 
			
		||||
    views:              Int64,
 | 
			
		||||
    description:        String,
 | 
			
		||||
    description_html:   String,
 | 
			
		||||
    length_seconds:     Int32,
 | 
			
		||||
    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] %>">
 | 
			
		||||
                        <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
 | 
			
		||||
                        <input required type="text" name="answer" type="text" placeholder="h:mm:ss">
 | 
			
		||||
 | 
			
		||||
                        <label>
 | 
			
		||||
                            <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">
 | 
			
		||||
                                <%= translate(locale, "Text CAPTCHA") %>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </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] %>">
 | 
			
		||||
                <% 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 %>
 | 
			
		||||
                        <label for="text_answer"><%= text_captcha.not_nil![:question] %></label>
 | 
			
		||||
                        <input required type="text" name="text_answer" type="text" placeholder="Answer">
 | 
			
		||||
                        <input type="hidden" name="captcha_type" value="image">
 | 
			
		||||
                        <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
 | 
			
		||||
                        <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">
 | 
			
		||||
                            <button type="submit" name="change_type" class="pure-button pure-button-primary" value="text">
 | 
			
		||||
                                <%= translate(locale, "Text CAPTCHA") %>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </label>
 | 
			
		||||
                    <% when "text" %>
 | 
			
		||||
                        <label>
 | 
			
		||||
                            <button type="submit" name="change_type" class="pure-button pure-button-primary" value="image">
 | 
			
		||||
                                <%= translate(locale, "Image CAPTCHA") %>
 | 
			
		||||
                            </a>
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </label>
 | 
			
		||||
                    <% end %>
 | 
			
		||||
                <% else %>
 | 
			
		||||
                    <button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
 | 
			
		||||
                        <%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>
 | 
			
		||||
                    </button>
 | 
			
		||||
                <% 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>
 | 
			
		||||
                    <% 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,50 +181,35 @@
 | 
			
		||||
            <% end %>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
            <i class="icon ion-logo-bitcoin"></i>
 | 
			
		||||
            BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</div>
 | 
			
		||||
          <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
            <i class="icon ion-logo-bitcoin"></i>
 | 
			
		||||
            BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</div>
 | 
			
		||||
          <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
            <i class="icon ion-logo-usd"></i>
 | 
			
		||||
            <a href="https://liberapay.com/omarroth">Liberapay</a>
 | 
			
		||||
             /
 | 
			
		||||
            <a href="https://patreon.com/omarroth">Patreon</a>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="content">
 | 
			
		||||
          <%= content %>
 | 
			
		||||
          <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
            <i class="icon ion-logo-javascript"></i>
 | 
			
		||||
            <a rel="jslicense" href="/licenses">
 | 
			
		||||
              <%= translate(locale, "View JavaScript license information.") %>
 | 
			
		||||
            </a>
 | 
			
		||||
            /
 | 
			
		||||
            <i class="icon ion-ios-paper"></i>
 | 
			
		||||
            <a href="/privacy">
 | 
			
		||||
              <%= translate(locale, "View privacy policy.") %>
 | 
			
		||||
            </a>
 | 
			
		||||
          </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>
 | 
			
		||||
              <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
                <i class="icon ion-logo-bitcoin"></i>
 | 
			
		||||
                BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</div>
 | 
			
		||||
              <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
                <i class="icon ion-logo-usd"></i>
 | 
			
		||||
                <a href="https://liberapay.com/omarroth">Liberapay</a>
 | 
			
		||||
                 / 
 | 
			
		||||
                <a href="https://patreon.com/omarroth">Patreon</a>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
                <i class="icon ion-logo-javascript"></i>
 | 
			
		||||
                <a rel="jslicense" href="/licenses">
 | 
			
		||||
                  <%= translate(locale, "View JavaScript license information.") %>
 | 
			
		||||
                </a>
 | 
			
		||||
                / 
 | 
			
		||||
                <i class="icon ion-ios-paper"></i>
 | 
			
		||||
                <a href="/privacy">
 | 
			
		||||
                  <%= translate(locale, "View privacy policy.") %>
 | 
			
		||||
                </a>
 | 
			
		||||
              </div> 
 | 
			
		||||
              <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
                <i class="icon ion-logo-github"></i>
 | 
			
		||||
                <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %>
 | 
			
		||||
                <i class="icon ion-logo-github"></i>
 | 
			
		||||
                <%= CURRENT_BRANCH %></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        <div class="pure-u-1 pure-u-md-2-24"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
          <div class="pure-u-1 pure-u-md-1-3">
 | 
			
		||||
            <i class="icon ion-logo-github"></i>
 | 
			
		||||
            <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %>
 | 
			
		||||
            <i class="icon ion-logo-github"></i>
 | 
			
		||||
            <%= CURRENT_BRANCH %></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  <script src="/js/ui.js"></script>
 | 
			
		||||
 | 
			
		||||
@ -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