Merge remote-tracking branch 'upstream/master' into side-menu

This commit is contained in:
Tommy Miland 2019-03-26 16:50:06 +01:00
commit 31b587baea
42 changed files with 1119 additions and 1370 deletions

View File

@ -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;
}

File diff suppressed because one or more lines are too long

View File

@ -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}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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);
}));

View File

@ -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

View File

@ -1,5 +1,3 @@
video_threads: 0
crawl_threads: 0
channel_threads: 1
feed_threads: 1
db:

View 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"

View 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;"

View File

@ -0,0 +1,3 @@
#!/bin/sh
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz;"

View File

@ -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)
);

View File

@ -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 :"
}

View File

@ -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: "
}

View File

@ -2,7 +2,7 @@ name: invidious
version: 0.15.0
authors:
- Omar Roth <omarroth@hotmail.com>
- Omar Roth <omarroth@protonmail.com>
targets:
invidious:

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -308,13 +308,13 @@ def template_youtube_comments(comments, locale)
<p>
<b>
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{child["author"]}</a>
</b>
</b>
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
<span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
|
<a href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "Youtube permalink of the comment")}">[YT]</a>
|
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
|
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
END_HTML
if child["creatorHeart"]?
@ -372,8 +372,8 @@ def template_reddit_comments(root, locale)
content = <<-END_HTML
<p>
<a href="javascript:void(0)" onclick="toggle_parent(this)">[ - ]</a>
<b><a href="https://www.reddit.com/user/#{author}">#{author}</a></b>
<a href="javascript:void(0)" onclick="toggle_parent(this)">[ - ]</a>
<b><a href="https://www.reddit.com/user/#{author}">#{author}</a></b>
#{translate(locale, "`x` points", number_with_separator(score))}
#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}
</p>

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
@ -296,7 +300,7 @@ def generate_captcha(key, db)
clock_svg = <<-END_SVG
<svg viewBox="0 0 100 100" width="200px">
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
@ -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

View File

@ -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"]

View File

@ -64,7 +64,7 @@
<%= rendered "components/item" %>
<% end %>
<% end %>
</div>
</div>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-1-5">

View File

@ -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>
@ -65,8 +67,10 @@
<p>
<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>
@ -100,8 +104,10 @@
<p>
<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 %>

View File

@ -1,4 +1,4 @@
<video style="outline:none;width:100%" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
<video style="outline:none;width:100%" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
id="player" class="video-js"
onmouseenter='this["data-title"]=this["title"];this["title"]=""'
onmouseleave='this["title"]=this["data-title"];this["data-title"]=""'
@ -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: {
@ -151,7 +152,7 @@ player.on('error', function(event) {
}
player.currentTime(currentTime);
player.playbackRate(playbackRate);
if (!paused) {
player.play();
}
@ -191,7 +192,7 @@ var bpb = player.getChild('bigPlayButton');
if (bpb) {
bpb.hide();
player.ready(function() {
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1);

View File

@ -1,14 +1,14 @@
<% if user %>
<% if subscriptions.includes? ucid %>
<p>
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
<b><%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %></b>
</a>
</p>
<% else %>
<p>
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
</a>
@ -16,7 +16,7 @@
<% end %>
<% else %>
<p>
<a id="subscribe" class="pure-button pure-button-primary"
<a id="subscribe" class="pure-button pure-button-primary"
href="/login?referer=<%= env.get("current_page") %>">
<b><%= translate(locale, "Login to subscribe to `x`", author) %></b>
</a>

View File

@ -4,13 +4,13 @@ if (subscribe_button.getAttribute('onclick')) {
}
function subscribe(timeouts = 0) {
subscribe_button = document.getElementById("subscribe");
subscribe_button = document.getElementById("subscribe");
if (timeouts > 10) {
console.log("Failed to subscribe.");
return;
}
var url = "/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>";
var xhr = new XMLHttpRequest();
xhr.responseType = "json";
@ -21,7 +21,7 @@ function subscribe(timeouts = 0) {
var fallback = subscribe_button.innerHTML;
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe").gsub("'", "\\'") %> | <%= sub_count_text %></b>'
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status != 200) {
@ -30,7 +30,7 @@ function subscribe(timeouts = 0) {
}
}
}
xhr.ontimeout = function() {
console.log("Subscribing timed out.");
@ -39,8 +39,8 @@ function subscribe(timeouts = 0) {
}
function unsubscribe(timeouts = 0) {
subscribe_button = document.getElementById("subscribe");
subscribe_button = document.getElementById("subscribe");
if (timeouts > 10) {
console.log("Failed to subscribe");
return;

View File

@ -20,7 +20,7 @@
</label>
<input type="file" id="import_youtube" name="import_youtube">
</div>
<div class="pure-control-group">
<label for="import_freetube"><%= translate(locale, "Import FreeTube subscriptions (.db)") %></label>
<input type="file" id="import_freetube" name="import_freetube">
@ -35,7 +35,7 @@
<label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label>
<input type="file" id="import_newpipe" name="import_newpipe">
</div>
<div class="pure-controls">
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button>
</div>

View File

@ -10,13 +10,13 @@
<title><%= HTML.escape(video.title) %> - Invidious</title>
<style>
#player {
position: fixed;
right: 0;
position: fixed;
right: 0;
bottom: 0;
min-width: 100%;
min-width: 100%;
min-height: 100%;
width: auto;
height: auto;
width: auto;
height: auto;
z-index: -100;
}
</style>

View File

@ -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>

View File

@ -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>

View File

@ -19,4 +19,4 @@
<%= rendered "components/item" %>
<% end %>
<% end %>
</div>
</div>

View File

@ -31,7 +31,7 @@
<%= rendered "components/item" %>
<% end %>
<% end %>
</div>
</div>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-1-5">

View File

@ -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>

View File

@ -5,15 +5,15 @@
<div class="h-box">
<%= Markdown.to_html(<<-END_PRIVACY_POLICY
## Privacy
This document concerns what data you provide to this website, the purpose of the data, how the data is stored, and how the data can be removed.
### Data you directly provide
Data that you provide to the website for the purpose of the site's operation (for example: an account name, account password, or channel subscription) will be stored in the website's database until the user decides to remove it. This data will not be intentionally shared with anyone or anything.
Information stored about a registered user is limited to:
- a list of session tokens for remaining logged in across devices
- the last time an account was updated (to provide accurate notifications)
- a list of video IDs identifying notifications from a user's subscriptions
@ -23,51 +23,51 @@
- a hashed password if applicable (not present on google accounts)
- a randomly generated token for providing an RSS feed of a user's subscriptions
- a list of video IDs identifying watched videos
The above list reflects [this code](https://github.com/omarroth/invidious/blob/master/src/invidious/users.cr#L14-L51).
Users can clear their watch history using the [clear watch history](/clear_watch_history) page.
If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.
### Data you passively provide
When you request any resource from this website (for example: a page, a font, an image, or an API endpoint) information about the request may be logged.
Information about a request is limited to:
- the time the request was made
- the status code of the response
- the method of the request
- the requested URL
- how long it took to complete the request.
No identifying information is logged, such as the visitor's cookie, user-agent, or IP address. Here are a couple lines to serve as an example:
```
2019-01-19 16:37:47 +00:00 200 GET /api/v1/comments/xrlETJYzH-c?format=html&hl=en-US 1345.88ms
2019-01-19 16:37:53 +00:00 200 GET /vi/r5P-f5arPXE/maxres.jpg 1085.41ms
2019-01-19 16:37:54 +00:00 200 GET /watch 7.04ms
```
This website does not store the visitor's user-agent or IP address and does not use fingerprinting, advertisements, or tracking of any form.
This website provides links to googlevideo.com to provide audio and video playback. googlevideo.com is owned by Google and is subject to their [privacy policy](https://policies.google.com/privacy).
### Data stored in your browser
This website uses browser cookies to authenticate registered users. This data consists of:
- An account token to keep you logged into the website between visits, which is sent when any page is loaded while you are logged in
This website also provides an option to store site preferences, such as the theme or locale, without an account. Using this feature will store a cookie in the visitor's browser containing their preferences. This cookie is sent on every request and does not contain any identifying information.
You can remove this data from your browser by logging out of this website, or by using your browser's cookie-related controls to delete the data.
### Removal of data
To remove data stored in your browser, you can log out of the website, or you can use your browser's cookie-related controls to delete the data.
To remove data that has been stored in the website's database, you can use the [delete my account](/delete_account) page.
END_PRIVACY_POLICY
)

View File

@ -8,7 +8,7 @@
<%= rendered "components/item" %>
<% end %>
<% end %>
</div>
</div>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-1-5">

View File

@ -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>

View File

@ -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">
@ -33,7 +33,7 @@
<div class="h-box">
<h1>
<%= HTML.escape(video.title) %>
<%= HTML.escape(video.title) %>
<% if params[:listen] %>
<a title="<%=translate(locale, "Video mode")%>" href="/watch?<%= env.params.query %>&listen=0">
<i class="icon ion-ios-videocam"></i>
@ -53,23 +53,23 @@
<div class="pure-u-1 pure-u-md-1-5">
<div class="h-box">
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p>
<form class="pure-form pure-form-stacked" action="/latest_version" method="get" rel="noopener" target="_blank">
<div class="pure-control-group">
<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 %>
@ -150,7 +150,7 @@
<% if params[:related_videos] %>
<div class="h-box">
<% if !rvs.empty? %>
<div id="continue" <% if plid %>style="display:none"<% end %>>
<div class="pure-control-group">
@ -187,7 +187,7 @@
<script>
<% if !rvs.empty? && !plid && params[:continue] %>
player.on('ended', function() {
location.assign("/watch?v="
location.assign("/watch?v="
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
+ "&continue=1"
<% if params[:listen] %>
@ -206,7 +206,7 @@ player.on('ended', function() {
function continue_autoplay(target) {
if (target.checked) {
player.on('ended', function() {
location.assign("/watch?v="
location.assign("/watch?v="
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
+ "&continue=1"
<% if params[:listen] %>
@ -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 %>"
@ -270,10 +270,10 @@ function get_playlist(timeouts = 0) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
playlist.innerHTML = xhr.response.playlistHtml;
if (xhr.response.nextVideo) {
player.on('ended', function() {
location.assign("/watch?v="
location.assign("/watch?v="
+ xhr.response.nextVideo
+ "&list=<%= plid %>"
<% if params[:listen] %>
@ -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();
@ -355,7 +355,7 @@ function get_reddit_comments(timeouts = 0) {
contentHtml: xhr.response.contentHtml
});
} else {
<% if preferences && preferences.comments[1] == "youtube" %>
<% if preferences && preferences.comments[1] == "youtube" %>
get_youtube_comments();
<% else %>
comments.innerHTML = fallback;
@ -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();
@ -416,7 +416,7 @@ function get_youtube_comments(timeouts = 0) {
comments.innerHTML = "";
}
} else {
<% if preferences && preferences.comments[1] == "youtube" %>
<% if preferences && preferences.comments[1] == "youtube" %>
get_youtube_comments();
<% else %>
comments.innerHTML = "";
@ -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;