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 { .loading {
display: inline-block;
animation: spin 2s linear infinite; animation: spin 2s linear infinite;
} }
@ -80,11 +81,15 @@ a.pure-button-primary:hover {
} }
div.thumbnail { div.thumbnail {
padding: 28.125%;
position: relative; position: relative;
box-sizing: border-box;
} }
img.thumbnail { img.thumbnail {
position: absolute;
width: 100%; width: 100%;
height: 100%;
left: 0; left: 0;
top: 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, .video-js .vjs-control-bar,
.vjs-menu-button-popup .vjs-menu .vjs-menu-content { .vjs-menu-button-popup .vjs-menu .vjs-menu-content {
background-color: rgba(35, 35, 35, 0.75); background-color: rgba(35, 35, 35, 0.75);
@ -326,29 +366,17 @@ img.thumbnail {
padding-top: 82vh; padding-top: 82vh;
} }
video.video-js {
position: absolute;
height: 100%;
}
#player-container { #player-container {
position: relative; position: relative;
padding-bottom: 82vh; padding-bottom: 82vh;
height: 0; 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 { .pure-control-group label {
word-wrap: normal; word-wrap: normal;
} }

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
/** /**
* videojs-share * videojs-share
* @version 2.0.1 * @version 3.0.0
* @copyright 2018 Mikhail Khazov <mkhazov.work@gmail.com> * @copyright 2018 Mikhail Khazov <mkhazov.work@gmail.com>
* @license MIT * @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 */ /* videojs-hotkeys v0.2.25 - 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})}); !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 channel_threads: 1
feed_threads: 1 feed_threads: 1
db: 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, ucid text,
author text, author text,
length_seconds integer, length_seconds integer,
live_now boolean,
premiere_timestamp timestamp with time zone,
CONSTRAINT channel_videos_id_key UNIQUE (id) 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 ?", "Clear watch history?": "Êtes-vous sûr de vouloir supprimer l'historique des vidéos regardées ?",
"Yes": "Oui", "Yes": "Oui",
"No": "Non", "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": "Importer",
"Import Invidious data": "Importer des données Invidious", "Import Invidious data": "Importer des données Invidious",
"Import YouTube subscriptions": "Importer des abonnements YouTube", "Import YouTube subscriptions": "Importer des abonnements YouTube",
@ -45,19 +45,19 @@
"Email:": "E-mail :", "Email:": "E-mail :",
"Google verification code:": "Code de vérification Google :", "Google verification code:": "Code de vérification Google :",
"Preferences": "Préférences", "Preferences": "Préférences",
"Player preferences": "Préférences du Lecteur", "Player preferences": "Préférences du lecteur",
"Always loop: ": "Lire en boucle : ", "Always loop: ": "Lire en boucle : ",
"Autoplay: ": "Lire Automatiquement : ", "Autoplay: ": "Lire automatiquement : ",
"Autoplay next video: ": "Lire automatiquement la vidéo suivante : ", "Autoplay next video: ": "Lire automatiquement la vidéo suivante : ",
"Listen by default: ": "Audio Uniquement par défaut : ", "Listen by default: ": "Audio uniquement : ",
"Proxy videos? ": "Souhaitez vous charger les vidéos à travers un proxy ?", "Proxy videos? ": "Charger les vidéos à travers un proxy ? ",
"Default speed: ": "Vitesse par défaut : ", "Default speed: ": "Vitesse par défaut : ",
"Preferred video quality: ": "Qualité vidéo souhaitée : ", "Preferred video quality: ": "Qualité vidéo souhaitée : ",
"Player volume: ": "Volume du lecteur : ", "Player volume: ": "Volume du lecteur : ",
"Default comments: ": "Source des Commentaires : ", "Default comments: ": "Source des commentaires : ",
"Default captions: ": "Sous-titres principal : ", "Default captions: ": "Sous-titres par défaut : ",
"Fallback captions: ": "Sous-titres secondaire : ", "Fallback captions: ": "Fallback captions: ",
"Show related videos? ": "Voir les vidéos liées à ce sujet ? ", "Show related videos? ": "Voir les vidéos liées ? ",
"Visual preferences": "Préférences du site", "Visual preferences": "Préférences du site",
"Dark mode: ": "Mode Sombre : ", "Dark mode: ": "Mode Sombre : ",
"Thin mode: ": "Mode Simplifié : ", "Thin mode: ": "Mode Simplifié : ",
@ -82,13 +82,13 @@
"Watch history": "Historique de visionnage", "Watch history": "Historique de visionnage",
"Delete account": "Supprimer votre compte", "Delete account": "Supprimer votre compte",
"Administrator preferences": "Préferences d'Administrateur", "Administrator preferences": "Préferences d'Administrateur",
"Default homepage: ": "Page d'accueil par defaut :", "Default homepage: ": "Page d'accueil par défaut : ",
"Feed menu: ": "Menu des Flux :", "Feed menu: ": "Menu des Flux : ",
"Top enabled? ": "Top activé ?", "Top enabled? ": "Top activé ? ",
"CAPTCHA enabled? ": "CAPTCHA activé ?", "CAPTCHA enabled? ": "CAPTCHA activé ? ",
"Login enabled? ": "Connexion activé ?", "Login enabled? ": "Connexion activé ? ",
"Registration enabled? ": "Inscription activé ?", "Registration enabled? ": "Inscription activée ? ",
"Report statistics? ": "Telemetrie activé ?", "Report statistics? ": "Télémétrie activé ? ",
"Save preferences": "Enregistrer les préférences", "Save preferences": "Enregistrer les préférences",
"Subscription manager": "Gestionnaire d'abonnement", "Subscription manager": "Gestionnaire d'abonnement",
"`x` subscriptions": "`x` abonnements", "`x` subscriptions": "`x` abonnements",
@ -108,11 +108,11 @@
"License: ": "Licence : ", "License: ": "Licence : ",
"Family friendly? ": "Tout Public ? ", "Family friendly? ": "Tout Public ? ",
"Wilson score: ": "Score de Wilson : ", "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 : ", "Whitelisted regions: ": "Régions en liste blanche : ",
"Blacklisted regions: ": "Régions sur liste noire : ", "Blacklisted regions: ": "Régions sur liste noire : ",
"Shared `x`": "Partagée `x`", "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 YouTube comments": "Voir les commentaires YouTube",
"View more comments on Reddit": "Voir plus de commentaires sur Reddit", "View more comments on Reddit": "Voir plus de commentaires sur Reddit",
"View `x` comments": "Voir `x` commentaires", "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.", "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", "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.", "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", "Invalid CAPTCHA": "CAPTCHA invalide",
"CAPTCHA is a required field": "Veuillez rentrez un CAPTCHA", "CAPTCHA is a required field": "Veuillez entrer un CAPTCHA",
"User ID is a required field": "Veuillez rentrez un Identifiant Utilisateur", "User ID is a required field": "Veuillez entrer un Identifiant Utilisateur",
"Password is a required field": "Veuillez rentrez un Mot de passe", "Password is a required field": "Veuillez entrer un Mot de passe",
"Invalid username or password": "Nom d'utilisateur ou mot de passe invalide", "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\"", "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", "Password cannot be empty": "Le mot de passe ne peut pas être vide",
@ -268,7 +268,7 @@
"`x` hours": "`x` heures", "`x` hours": "`x` heures",
"`x` minutes": "`x` minutes", "`x` minutes": "`x` minutes",
"`x` seconds": "`x` secondes", "`x` seconds": "`x` secondes",
"Fallback comments: ": "Commentaires secondaires : ", "Fallback comments: ": "Fallback comments: ",
"Popular": "Populaire", "Popular": "Populaire",
"Top": "Top", "Top": "Top",
"About": "A Propos", "About": "A Propos",
@ -289,5 +289,5 @@
"Video mode": "Mode Vidéo", "Video mode": "Mode Vidéo",
"Videos": "Vidéos", "Videos": "Vidéos",
"Playlists": "Liste de lecture", "Playlists": "Liste de lecture",
"Current version: ": "Version actuelle :" "Current version: ": "Version :"
} }

View File

@ -1,293 +1,293 @@
{ {
"`x` subscribers": "`x` subskrybcji", "`x` subscribers": "`x` subskrybcji",
"`x` videos": "`x` filmów", "`x` videos": "`x` filmów",
"LIVE": "NA ŻYWO", "LIVE": "NA ŻYWO",
"Shared `x` ago": "Udostępniono `x` temu", "Shared `x` ago": "Udostępniono `x` temu",
"Unsubscribe": "Odsubskrybuj", "Unsubscribe": "Odsubskrybuj",
"Subscribe": "Subskrybuj", "Subscribe": "Subskrybuj",
"Login to subscribe to `x`": "Zaloguj się, aby subskrybować `x`", "Login to subscribe to `x`": "Zaloguj się, aby subskrybować `x`",
"View channel on YouTube": "Wyświetl kanał na YouTube", "View channel on YouTube": "Wyświetl kanał na YouTube",
"newest": "najnowsze", "newest": "najnowsze",
"oldest": "najstarsze", "oldest": "najstarsze",
"popular": "popularne", "popular": "popularne",
"last": "", "last": "ostatnie",
"Next page": "Następna strona", "Next page": "Następna strona",
"Previous page": "Poprzednia strona", "Previous page": "Poprzednia strona",
"Clear watch history?": "Wyczyścić historię?", "Clear watch history?": "Wyczyścić historię?",
"Yes": "Tak", "Yes": "Tak",
"No": "Nie", "No": "Nie",
"Import and Export Data": "Import i eksport danych", "Import and Export Data": "Import i eksport danych",
"Import": "Import", "Import": "Import",
"Import Invidious data": "Importuj dane Invidious", "Import Invidious data": "Importuj dane Invidious",
"Import YouTube subscriptions": "Importuj subskrybcje z YouTube", "Import YouTube subscriptions": "Importuj subskrybcje z YouTube",
"Import FreeTube subscriptions (.db)": "Importuj subskrybcje z FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Importuj subskrybcje z FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importuj subskrybcje z NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importuj subskrybcje z NewPipe (.json)",
"Import NewPipe data (.zip)": "Importuj dane NewPipe (.zip)", "Import NewPipe data (.zip)": "Importuj dane NewPipe (.zip)",
"Export": "Eksport", "Export": "Eksport",
"Export subscriptions as OPML": "Eksportuj subskrybcje jako OPML", "Export subscriptions as OPML": "Eksportuj subskrybcje jako OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrybcje jako OPML (dla NewPipe i FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrybcje jako OPML (dla NewPipe i FreeTube)",
"Export data as JSON": "Eksportuj dane jako JSON", "Export data as JSON": "Eksportuj dane jako JSON",
"Delete account?": "Usunąć konto?", "Delete account?": "Usunąć konto?",
"History": "Historia", "History": "Historia",
"An alternative front-end to YouTube": "Alternatywny front-end dla YouTube", "An alternative front-end to YouTube": "Alternatywny front-end dla YouTube",
"JavaScript license information": "Informacja o licencji JavaScript", "JavaScript license information": "Informacja o licencji JavaScript",
"source": "źródło", "source": "źródło",
"Login": "Zaloguj", "Login": "Zaloguj",
"Login/Register": "Zaloguj/Zarejestruj", "Login/Register": "Zaloguj/Zarejestruj",
"Login to Google": "Zaloguj do Google", "Login to Google": "Zaloguj do Google",
"User ID:": "ID użytkownika:", "User ID:": "ID użytkownika:",
"Password:": "Hasło:", "Password:": "Hasło:",
"Time (h:mm:ss):": "Godzina (h:mm:ss):", "Time (h:mm:ss):": "Godzina (h:mm:ss):",
"Text CAPTCHA": "Tekst CAPTCHA", "Text CAPTCHA": "Tekst CAPTCHA",
"Image CAPTCHA": "Obraz CAPTCHA", "Image CAPTCHA": "Obraz CAPTCHA",
"Sign In": "Zaloguj się", "Sign In": "Zaloguj się",
"Register": "Zarejestruj się", "Register": "Zarejestruj się",
"Email:": "Email:", "Email:": "Email:",
"Google verification code:": "Kod weryfikacyjny Google:", "Google verification code:": "Kod weryfikacyjny Google:",
"Preferences": "Preferencje", "Preferences": "Preferencje",
"Player preferences": "Ustawienia odtwarzacza", "Player preferences": "Ustawienia odtwarzacza",
"Always loop: ": "Zawsze zapętlaj: ", "Always loop: ": "Zawsze zapętlaj: ",
"Autoplay: ": "Autoodtwarzanie: ", "Autoplay: ": "Autoodtwarzanie: ",
"Autoplay next video: ": "Odtwórz następny film: ", "Autoplay next video: ": "Odtwórz następny film: ",
"Listen by default: ": "Tryb dźwiękowy: ", "Listen by default: ": "Tryb dźwiękowy: ",
"Proxy videos? ": "", "Proxy videos? ": "Filmy przez proxy? ",
"Default speed: ": "Domyślna prędkość: ", "Default speed: ": "Domyślna prędkość: ",
"Preferred video quality: ": "Preferowana jakość filmów: ", "Preferred video quality: ": "Preferowana jakość filmów: ",
"Player volume: ": "Głośność odtwarzacza: ", "Player volume: ": "Głośność odtwarzacza: ",
"Default comments: ": "Domyślne komentarze: ", "Default comments: ": "Domyślne komentarze: ",
"Default captions: ": "Domyślne napisy: ", "Default captions: ": "Domyślne napisy: ",
"Fallback captions: ": "Zastępcze napisy: ", "Fallback captions: ": "Zastępcze napisy: ",
"Show related videos? ": "Pokaż powiązane filmy? ", "Show related videos? ": "Pokaż powiązane filmy? ",
"Visual preferences": "Preferencje Wizualne", "Visual preferences": "Preferencje Wizualne",
"Dark mode: ": "Ciemny motyw: ", "Dark mode: ": "Ciemny motyw: ",
"Thin mode: ": "Tryb minimalny: ", "Thin mode: ": "Tryb minimalny: ",
"Subscription preferences": "Preferencje subskrybcji", "Subscription preferences": "Preferencje subskrybcji",
"Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ", "Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
"Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ", "Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ",
"Sort videos by: ": "Sortuj filmy: ", "Sort videos by: ": "Sortuj filmy: ",
"published": "po czasie publikacji", "published": "po czasie publikacji",
"published - reverse": "po czasie publikacji od najstarszych", "published - reverse": "po czasie publikacji od najstarszych",
"alphabetically": "alfabetycznie", "alphabetically": "alfabetycznie",
"alphabetically - reverse": "alfabetycznie od tyłu", "alphabetically - reverse": "alfabetycznie od tyłu",
"channel name": "po nazwie kanału", "channel name": "po nazwie kanału",
"channel name - reverse": "po nazwie kanału od tył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 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 latest unwatched video from channel: ": "Pokazuj tylko najnowszy nie obejrzany film z kanału: ",
"Only show unwatched: ": "Pokazuj tylko nie obejrzane: ", "Only show unwatched: ": "Pokazuj tylko nie obejrzane: ",
"Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ", "Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ",
"Data preferences": "Preferencje danych", "Data preferences": "Preferencje danych",
"Clear watch history": "Wyczyść historię", "Clear watch history": "Wyczyść historię",
"Import/Export data": "Import/Eksport danych", "Import/Export data": "Import/Eksport danych",
"Manage subscriptions": "Organizuj subskrybcje", "Manage subscriptions": "Organizuj subskrybcje",
"Watch history": "Historia", "Watch history": "Historia",
"Delete account": "Usuń konto", "Delete account": "Usuń konto",
"Administrator preferences": "Preferencje administratora", "Administrator preferences": "Preferencje administratora",
"Default homepage: ": "Domyślna strona główna: ", "Default homepage: ": "Domyślna strona główna: ",
"Feed menu: ": "", "Feed menu: ": "",
"Top enabled? ": "", "Top enabled? ": "",
"CAPTCHA enabled? ": "CAPTCHA aktywna? ", "CAPTCHA enabled? ": "CAPTCHA aktywna? ",
"Login enabled? ": "Logowanie włączone? ", "Login enabled? ": "Logowanie włączone? ",
"Registration enabled? ": "Rejestracja włączona? ", "Registration enabled? ": "Rejestracja włączona? ",
"Report statistics? ": "Raportować statystyki? ", "Report statistics? ": "Raportować statystyki? ",
"Save preferences": "Zapisz preferencje", "Save preferences": "Zapisz preferencje",
"Subscription manager": "Manager subskrybcji", "Subscription manager": "Manager subskrybcji",
"`x` subscriptions": "`x` subskrybcji", "`x` subscriptions": "`x` subskrybcji",
"Import/Export": "Import/Eksport", "Import/Export": "Import/Eksport",
"unsubscribe": "odsubskrybuj", "unsubscribe": "odsubskrybuj",
"Subscriptions": "Subskrybcje", "Subscriptions": "Subskrybcje",
"`x` unseen notifications": "`x` niewidzianych powiadomień", "`x` unseen notifications": "`x` niewidzianych powiadomień",
"search": "szukaj", "search": "szukaj",
"Sign out": "Wyloguj", "Sign out": "Wyloguj",
"Released under the AGPLv3 by Omar Roth.": "Wydano na licencji AGPLv3 przez Omar Roth.", "Released under the AGPLv3 by Omar Roth.": "Wydano na licencji AGPLv3 przez Omar Roth.",
"Source available here.": "Kod źródłowy dostępny tutaj.", "Source available here.": "Kod źródłowy dostępny tutaj.",
"View JavaScript license information.": "Wyświetl informację o licencji JavaScript.", "View JavaScript license information.": "Wyświetl informację o licencji JavaScript.",
"View privacy policy.": "", "View privacy policy.": "Polityka prywatności.",
"Trending": "Na czasie", "Trending": "Na czasie",
"Watch video on Youtube": "Zobacz film na YouTube", "Watch video on Youtube": "Zobacz film na YouTube",
"Genre: ": "Gatunek: ", "Genre: ": "Gatunek: ",
"License: ": "Licencja: ", "License: ": "Licencja: ",
"Family friendly? ": "Przyjazny rodzinie? ", "Family friendly? ": "Przyjazny rodzinie? ",
"Wilson score: ": "Punktacja Wilsona: ", "Wilson score: ": "Punktacja Wilsona: ",
"Engagement: ": "Zaangażowanie: ", "Engagement: ": "Zaangażowanie: ",
"Whitelisted regions: ": "Dostępny na obszarach: ", "Whitelisted regions: ": "Dostępny na obszarach: ",
"Blacklisted regions: ": "Niedostępny na obszarach: ", "Blacklisted regions: ": "Niedostępny na obszarach: ",
"Shared `x`": "Udostępniono `x`", "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.", "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 YouTube comments": "Wyświetl komentarze z YouTube",
"View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie", "View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
"View `x` comments": "Wyświetl `x` komentarzy", "View `x` comments": "Wyświetl `x` komentarzy",
"View Reddit comments": "Wyświetl komentarze z Redditta", "View Reddit comments": "Wyświetl komentarze z Redditta",
"Hide replies": "Ukryj odpowiedzi", "Hide replies": "Ukryj odpowiedzi",
"Show replies": "Pokaż odpowiedzi", "Show replies": "Pokaż odpowiedzi",
"Incorrect password": "Niepoprawne hasło", "Incorrect password": "Niepoprawne hasło",
"Quota exceeded, try again in a few hours": "Przekroczony limit zapytań, spróbuj ponownie za kilka godzin", "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.", "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", "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.", "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 answer": "Niepoprawna odpowiedź",
"Invalid CAPTCHA": "CAPTCHA wykonane błędnie", "Invalid CAPTCHA": "CAPTCHA wykonane błędnie",
"CAPTCHA is a required field": "CAPTCHA jest polem wymaganym", "CAPTCHA is a required field": "CAPTCHA jest polem wymaganym",
"User ID is a required field": "ID użytkownika 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", "Password is a required field": "Hasło jest polem wymaganym",
"Invalid username or password": "Niepoprawny login lub hasło", "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\"", "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 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", "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ć", "Please sign in": "Proszę się zalogować",
"Invidious Private Feed for `x`": "", "Invidious Private Feed for `x`": "",
"channel:`x`": "kanał:`x", "channel:`x`": "kanał:`x",
"Deleted or invalid channel": "Usunięty lub niepoprawny kanał", "Deleted or invalid channel": "Usunięty lub niepoprawny kanał",
"This channel does not exist.": "Ten kanał nie istnieje.", "This channel does not exist.": "Ten kanał nie istnieje.",
"Could not get channel info.": "Nie udało się uzyskać informacji o kanale.", "Could not get channel info.": "Nie udało się uzyskać informacji o kanale.",
"Could not fetch comments": "Nie udało się pobrać komentarzy", "Could not fetch comments": "Nie udało się pobrać komentarzy",
"View `x` replies": "Wyświetl `x` odpowiedzi", "View `x` replies": "Wyświetl `x` odpowiedzi",
"`x` ago": "`x` temu", "`x` ago": "`x` temu",
"Load more": "Wczytaj więcej", "Load more": "Wczytaj więcej",
"`x` points": "`x` punktów", "`x` points": "`x` punktów",
"Could not create mix.": "Nie udało się utworzyć miksu.", "Could not create mix.": "Nie udało się utworzyć miksu.",
"Playlist is empty": "Lista odtwarzania jest pusta", "Playlist is empty": "Lista odtwarzania jest pusta",
"Invalid playlist.": "Niepoprawna lista.", "Invalid playlist.": "Niepoprawna lista.",
"Playlist does not exist.": "Lista odtwarzania nie istnieje.", "Playlist does not exist.": "Lista odtwarzania nie istnieje.",
"Could not pull trending pages.": "Nie udało się pobrać strony na czasie.", "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 \"challenge\" is a required field": "Ukryte pole \"wyzwanie\" jest polem wymaganym",
"Hidden field \"token\" is a required field": "Ukryte pole \"token\" jest polem wymaganym", "Hidden field \"token\" is a required field": "Ukryte pole \"token\" jest polem wymaganym",
"Invalid challenge": "Niepoprawne wyzwanie", "Invalid challenge": "Niepoprawne wyzwanie",
"Invalid token": "Niepoprawny token", "Invalid token": "Niepoprawny token",
"Invalid user": "Niepoprawny użytkownik", "Invalid user": "Niepoprawny użytkownik",
"Token is expired, please try again": "Token wygasł, spróbuj ponownie", "Token is expired, please try again": "Token wygasł, spróbuj ponownie",
"English": "angielski", "English": "angielski",
"English (auto-generated)": "angielski (automatycznie generowane)", "English (auto-generated)": "angielski (automatycznie generowane)",
"Afrikaans": "afrykanerski", "Afrikaans": "afrykanerski",
"Albanian": "albański", "Albanian": "albański",
"Amharic": "amharski", "Amharic": "amharski",
"Arabic": "arabski", "Arabic": "arabski",
"Armenian": "armeński", "Armenian": "armeński",
"Azerbaijani": "azerski", "Azerbaijani": "azerski",
"Bangla": "bengalski", "Bangla": "bengalski",
"Basque": "baskijski", "Basque": "baskijski",
"Belarusian": "białoruski", "Belarusian": "białoruski",
"Bosnian": "bośniacki", "Bosnian": "bośniacki",
"Bulgarian": "bułgarski", "Bulgarian": "bułgarski",
"Burmese": "birmański", "Burmese": "birmański",
"Catalan": "kataloński", "Catalan": "kataloński",
"Cebuano": "cebuański", "Cebuano": "cebuański",
"Chinese (Simplified)": "chiński (uproszczony)", "Chinese (Simplified)": "chiński (uproszczony)",
"Chinese (Traditional)": "chiński (tradycyjny)", "Chinese (Traditional)": "chiński (tradycyjny)",
"Corsican": "korsykański", "Corsican": "korsykański",
"Croatian": "chorwacki", "Croatian": "chorwacki",
"Czech": "czeski", "Czech": "czeski",
"Danish": "duński", "Danish": "duński",
"Dutch": "holenderski", "Dutch": "holenderski",
"Esperanto": "esperanto", "Esperanto": "esperanto",
"Estonian": "estoński", "Estonian": "estoński",
"Filipino": "filipiński", "Filipino": "filipiński",
"Finnish": "fiński", "Finnish": "fiński",
"French": "francuski", "French": "francuski",
"Galician": "galicyjski", "Galician": "galicyjski",
"Georgian": "gruziński", "Georgian": "gruziński",
"German": "niemiecki", "German": "niemiecki",
"Greek": "grecki", "Greek": "grecki",
"Gujarati": "gudźarati", "Gujarati": "gudźarati",
"Haitian Creole": "kreolski haitański", "Haitian Creole": "kreolski haitański",
"Hausa": "hausa", "Hausa": "hausa",
"Hawaiian": "hawajski", "Hawaiian": "hawajski",
"Hebrew": "hebrajski", "Hebrew": "hebrajski",
"Hindi": "hindi", "Hindi": "hindi",
"Hmong": "hmong", "Hmong": "hmong",
"Hungarian": "węgierski", "Hungarian": "węgierski",
"Icelandic": "islandzki", "Icelandic": "islandzki",
"Igbo": "ibo", "Igbo": "ibo",
"Indonesian": "indonezyjski", "Indonesian": "indonezyjski",
"Irish": "irlandzki", "Irish": "irlandzki",
"Italian": "włoski", "Italian": "włoski",
"Japanese": "japoński", "Japanese": "japoński",
"Javanese": "jawajski", "Javanese": "jawajski",
"Kannada": "kannada", "Kannada": "kannada",
"Kazakh": "kazachski", "Kazakh": "kazachski",
"Khmer": "khmerski", "Khmer": "khmerski",
"Korean": "koreański", "Korean": "koreański",
"Kurdish": "kurdyjski", "Kurdish": "kurdyjski",
"Kyrgyz": "kirgiski", "Kyrgyz": "kirgiski",
"Lao": "laotański", "Lao": "laotański",
"Latin": "łaciński", "Latin": "łaciński",
"Latvian": "łotewski", "Latvian": "łotewski",
"Lithuanian": "litewski", "Lithuanian": "litewski",
"Luxembourgish": "luksemburski", "Luxembourgish": "luksemburski",
"Macedonian": "macedoński", "Macedonian": "macedoński",
"Malagasy": "malgaski", "Malagasy": "malgaski",
"Malay": "malajski", "Malay": "malajski",
"Malayalam": "malajalam", "Malayalam": "malajalam",
"Maltese": "maltański", "Maltese": "maltański",
"Maori": "maoryski", "Maori": "maoryski",
"Marathi": "marathi", "Marathi": "marathi",
"Mongolian": "mongolski", "Mongolian": "mongolski",
"Nepali": "nepalski", "Nepali": "nepalski",
"Norwegian": "norweski", "Norwegian": "norweski",
"Nyanja": "njandża", "Nyanja": "njandża",
"Pashto": "paszto", "Pashto": "paszto",
"Persian": "perski", "Persian": "perski",
"Polish": "polski", "Polish": "polski",
"Portuguese": "portugalski", "Portuguese": "portugalski",
"Punjabi": "pendżabski", "Punjabi": "pendżabski",
"Romanian": "rumuński", "Romanian": "rumuński",
"Russian": "rosyjski", "Russian": "rosyjski",
"Samoan": "samoański", "Samoan": "samoański",
"Scottish Gaelic": "gaelicki szkocki", "Scottish Gaelic": "gaelicki szkocki",
"Serbian": "serbski", "Serbian": "serbski",
"Shona": "shona", "Shona": "shona",
"Sindhi": "sindhi", "Sindhi": "sindhi",
"Sinhala": "syngaleski", "Sinhala": "syngaleski",
"Slovak": "słowacki", "Slovak": "słowacki",
"Slovenian": "słoweński", "Slovenian": "słoweński",
"Somali": "somalijski", "Somali": "somalijski",
"Southern Sotho": "sotho południowy", "Southern Sotho": "sotho południowy",
"Spanish": "hiszpański", "Spanish": "hiszpański",
"Spanish (Latin America)": "hiszpański (ameryka łacińska)", "Spanish (Latin America)": "hiszpański (ameryka łacińska)",
"Sundanese": "sundajski", "Sundanese": "sundajski",
"Swahili": "suahili", "Swahili": "suahili",
"Swedish": "szwedzki", "Swedish": "szwedzki",
"Tajik": "tadżycki", "Tajik": "tadżycki",
"Tamil": "tamilski", "Tamil": "tamilski",
"Telugu": "telugu", "Telugu": "telugu",
"Thai": "tajski", "Thai": "tajski",
"Turkish": "turecki", "Turkish": "turecki",
"Ukrainian": "ukraiński", "Ukrainian": "ukraiński",
"Urdu": "urdu", "Urdu": "urdu",
"Uzbek": "uzbecki", "Uzbek": "uzbecki",
"Vietnamese": "wietnamski", "Vietnamese": "wietnamski",
"Welsh": "walijski", "Welsh": "walijski",
"Western Frisian": "zachodniofryzyjski", "Western Frisian": "zachodniofryzyjski",
"Xhosa": "xhosa", "Xhosa": "xhosa",
"Yiddish": "jidysz", "Yiddish": "jidysz",
"Yoruba": "joruba", "Yoruba": "joruba",
"Zulu": "zuluski", "Zulu": "zuluski",
"`x` years": "`x` lat", "`x` years": "`x` lat",
"`x` months": "`x` miesięcy", "`x` months": "`x` miesięcy",
"`x` weeks": "`x` tygodni", "`x` weeks": "`x` tygodni",
"`x` days": "`x` dni", "`x` days": "`x` dni",
"`x` hours": "`x` godzin", "`x` hours": "`x` godzin",
"`x` minutes": "`x` minut", "`x` minutes": "`x` minut",
"`x` seconds": "`x` sekund", "`x` seconds": "`x` sekund",
"Fallback comments: ": "Zastępcze komentarze: ", "Fallback comments: ": "Zastępcze komentarze: ",
"Popular": "Popularne", "Popular": "Popularne",
"Top": "Na czasie", "Top": "Najczęściej oglądane",
"About": "Informacje", "About": "Informacje",
"Rating: ": "Ocena: ", "Rating: ": "Ocena: ",
"Language: ": "Język: ", "Language: ": "Język: ",
"Default": "Domyślnie", "Default": "Domyślnie",
"Music": "Muzyka", "Music": "Muzyka",
"Gaming": "Gry", "Gaming": "Gry",
"News": "Wiadomości", "News": "Wiadomości",
"Movies": "Filmy", "Movies": "Filmy",
"Download": "Pobierz", "Download": "Pobierz",
"Download as: ": "Pobierz jako: ", "Download as: ": "Pobierz jako: ",
"%A %B %-d, %Y": "", "%A %B %-d, %Y": "",
"(edited)": "(edytowany)", "(edited)": "(edytowany)",
"Youtube permalink of the comment": "Odnośnik bezpośredni do komentarza na YouTube", "Youtube permalink of the comment": "Odnośnik bezpośredni do komentarza na YouTube",
"`x` marked it with a ❤": "'x' oznaczonych ❤", "`x` marked it with a ❤": "'x' oznaczonych ❤",
"Audio mode": "Tryb audio", "Audio mode": "Tryb audio",
"Video mode": "Tryb wideo", "Video mode": "Tryb wideo",
"Videos": "Filmy", "Videos": "Filmy",
"Playlists": "Playlisty", "Playlists": "Playlisty",
"Current version: ": "Aktualna wersja: " "Current version: ": "Aktualna wersja: "
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -10,13 +10,15 @@ end
class ChannelVideo class ChannelVideo
add_mapping({ add_mapping({
id: String, id: String,
title: String, title: String,
published: Time, published: Time,
updated: Time, updated: Time,
ucid: String, ucid: String,
author: String, author: String,
length_seconds: {type: Int32, default: 0}, length_seconds: {type: Int32, default: 0},
live_now: {type: Bool, default: false},
premiere_timestamp: {type: Time?, default: nil},
}) })
end 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 author = entry.xpath_node("author/name").not_nil!.content
ucid = entry.xpath_node("channelid").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 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 \ db.exec("UPDATE users SET notifications = notifications || $1 \
WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, ucid) 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 video_array = video.to_a
args = arg_array(video_array) 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}) \ db.exec("INSERT INTO channel_videos VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \ 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 end
else else
page = 1 page = 1
@ -157,7 +179,17 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
end end
count = nodeset.size 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| videos.each do |video|
ids << video.id ids << video.id
@ -170,8 +202,12 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
video_array = video.to_a video_array = video.to_a
args = arg_array(video_array) args = arg_array(video_array)
db.exec("INSERT INTO channel_videos VALUES (#{args}) ON CONFLICT (id) DO UPDATE SET title = $2, \ # We don't include the 'premire_timestamp' here because channel pages don't include them,
published = $3, updated = $4, ucid = $5, author = $6, length_seconds = $7", video_array) # 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
end end

View File

@ -308,13 +308,13 @@ def template_youtube_comments(comments, locale)
<p> <p>
<b> <b>
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{child["author"]}</a> <a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{child["author"]}</a>
</b> </b>
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p> <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> <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> <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 END_HTML
if child["creatorHeart"]? if child["creatorHeart"]?
@ -372,8 +372,8 @@ def template_reddit_comments(root, locale)
content = <<-END_HTML content = <<-END_HTML
<p> <p>
<a href="javascript:void(0)" onclick="toggle_parent(this)">[ - ]</a> <a href="javascript:void(0)" onclick="toggle_parent(this)">[ - ]</a>
<b><a href="https://www.reddit.com/user/#{author}">#{author}</a></b> <b><a href="https://www.reddit.com/user/#{author}">#{author}</a></b>
#{translate(locale, "`x` points", number_with_separator(score))} #{translate(locale, "`x` points", number_with_separator(score))}
#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}
</p> </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 class Config
YAML.mapping({ 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) 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 feed_threads: Int32, # Number of threads to use for updating feeds
db: NamedTuple( # Database configuration db: NamedTuple( # Database configuration
@ -28,61 +26,6 @@ user: String,
}) })
end 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) def rank_videos(db, n)
top = [] of {Float64, String} top = [] of {Float64, String}
@ -325,6 +268,11 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
paid = true paid = true
end 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( items << SearchVideo.new(
title: title, title: title,
id: id, id: id,
@ -337,7 +285,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
length_seconds: length_seconds, length_seconds: length_seconds,
live_now: live_now, live_now: live_now,
paid: paid, paid: paid,
premium: premium premium: premium,
premiere_timestamp: premiere_timestamp
) )
end end
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) def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
max_channel = Channel(Int32).new max_channel = Channel(Int32).new
@ -82,30 +34,14 @@ def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
end end
end end
end end
sleep 1.minute
end end
end end
max_channel.send(max_threads) max_channel.send(max_threads)
end 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) def refresh_feeds(db, logger, max_threads = 1)
max_channel = Channel(Int32).new max_channel = Channel(Int32).new
@ -129,15 +65,26 @@ def refresh_feeds(db, logger, max_threads = 1)
active_threads += 1 active_threads += 1
spawn do spawn do
begin 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}") db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
rescue ex rescue ex
# Create view if it doesn't exist # Create view if it doesn't exist
if ex.message.try &.ends_with? "does not exist" if ex.message.try &.ends_with?("does not exist")
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \ # While iterating through, we may have an email stored from a deleted account
SELECT * FROM channel_videos WHERE \ if db.query_one?("SELECT true FROM users WHERE email = $1", email, as: Bool)
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \ db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
ORDER BY published DESC;") SELECT * FROM channel_videos WHERE \
logger.write("CREATE #{view_name}") ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;")
logger.write("CREATE #{view_name}\n")
end
else else
logger.write("REFRESH #{email} : #{ex.message}\n") logger.write("REFRESH #{email} : #{ex.message}\n")
end end
@ -147,6 +94,8 @@ def refresh_feeds(db, logger, max_threads = 1)
end end
end end
end end
sleep 1.minute
end end
end end
@ -169,7 +118,6 @@ def subscribe_to_feeds(db, logger, key, config)
end end
sleep 1.minute sleep 1.minute
Fiber.yield
end end
end end
end end
@ -200,7 +148,7 @@ def pull_top_videos(config, db)
end end
yield videos yield videos
Fiber.yield sleep 1.minute
end end
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 ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse
yield videos yield videos
Fiber.yield sleep 1.minute
end end
end end
@ -228,6 +176,7 @@ def update_decrypt_function
end end
yield decrypt_function yield decrypt_function
sleep 1.minute
end end
end end
@ -239,7 +188,8 @@ def find_working_proxies(regions)
# proxies = filter_proxies(proxies) # proxies = filter_proxies(proxies)
yield region, proxies yield region, proxies
Fiber.yield
end end
sleep 1.minute
end end
end end

View File

@ -8,6 +8,7 @@ class PlaylistVideo
published: Time, published: Time,
playlists: Array(String), playlists: Array(String),
index: Int32, index: Int32,
live_now: Bool,
}) })
end end
@ -101,8 +102,10 @@ def extract_playlist(plid, nodeset, index)
anchor = video.xpath_node(%q(.//td[@class="pl-video-time"]/div/div[1])) anchor = video.xpath_node(%q(.//td[@class="pl-video-time"]/div/div[1]))
if anchor && !anchor.content.empty? if anchor && !anchor.content.empty?
length_seconds = decode_length_seconds(anchor.content) length_seconds = decode_length_seconds(anchor.content)
live_now = false
else else
length_seconds = 0 length_seconds = 0
live_now = true
end end
videos << PlaylistVideo.new( videos << PlaylistVideo.new(
@ -114,6 +117,7 @@ def extract_playlist(plid, nodeset, index)
published: Time.now, published: Time.now,
playlists: [plid], playlists: [plid],
index: index + offset, index: index + offset,
live_now: live_now
) )
end end

View File

@ -1,17 +1,18 @@
class SearchVideo class SearchVideo
add_mapping({ add_mapping({
title: String, title: String,
id: String, id: String,
author: String, author: String,
ucid: String, ucid: String,
published: Time, published: Time,
views: Int64, views: Int64,
description: String, description: String,
description_html: String, description_html: String,
length_seconds: Int32, length_seconds: Int32,
live_now: Bool, live_now: Bool,
paid: Bool, paid: Bool,
premium: Bool, premium: Bool,
premiere_timestamp: Time?,
}) })
end 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 = OpenSSL::HMAC.digest(:sha256, key, challenge)
challenge = Base64.urlsafe_encode(challenge) challenge = Base64.urlsafe_encode(challenge)
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool) if nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", nonce, as: {String, Time})
db.exec("DELETE FROM nonces * WHERE nonce = $1", nonce) 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 else
raise translate(locale, "Invalid token") raise translate(locale, "Invalid token")
end end
@ -270,7 +274,7 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
end end
if challenge_user_id != user_id if challenge_user_id != user_id
raise translate(locale, "Invalid user") raise translate(locale, "Invalid token")
end end
if expire < Time.now.to_unix if expire < Time.now.to_unix
@ -296,7 +300,7 @@ def generate_captcha(key, db)
clock_svg = <<-END_SVG clock_svg = <<-END_SVG
<svg viewBox="0 0 100 100" width="200px"> <svg viewBox="0 0 100 100" width="200px">
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle> <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="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="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> <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 = "#{hour}:#{minute.to_s.rjust(2, '0')}:#{second.to_s.rjust(2, '0')}"
answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer) answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer)
challenge, token = create_response(answer, "sign_in", key, db) return {
question: image,
return {image: image, challenge: challenge, token: token} 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 end

View File

@ -250,6 +250,63 @@ class Video
end end
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 def keywords
keywords = self.player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a keywords = self.player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a
keywords ||= [] of String keywords ||= [] of String
@ -644,6 +701,10 @@ def fetch_video(id, proxies, region)
raise "Video unavailable." raise "Video unavailable."
end end
if !info["title"]?
raise "Video unavailable."
end
title = info["title"] title = info["title"]
author = info["author"] author = info["author"]
ucid = info["ucid"] ucid = info["ucid"]

View File

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

View File

@ -39,7 +39,9 @@
<% else %> <% else %>
<div class="thumbnail"> <div class="thumbnail">
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/> <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if item.length_seconds != 0 %>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p> <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %>
</div> </div>
<% end %> <% end %>
<p><%= item.title %></p> <p><%= item.title %></p>
@ -55,7 +57,7 @@
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/> <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if item.responds_to?(:live_now) && item.live_now %> <% if item.responds_to?(:live_now) && item.live_now %>
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> <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> <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %> <% end %>
</div> </div>
@ -65,8 +67,10 @@
<p> <p>
<b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b> <b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
</p> </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> <h5><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></h5>
<% end %> <% end %>
<% else %> <% else %>
@ -90,7 +94,7 @@
<% end %> <% end %>
<% if item.responds_to?(:live_now) && item.live_now %> <% if item.responds_to?(:live_now) && item.live_now %>
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p> <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> <p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %> <% end %>
</div> </div>
@ -100,8 +104,10 @@
<p> <p>
<b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b> <b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
</p> </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> <h5><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></h5>
<% end %> <% end %>
<% 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" id="player" class="video-js"
onmouseenter='this["data-title"]=this["title"];this["title"]=""' onmouseenter='this["data-title"]=this["title"];this["title"]=""'
onmouseleave='this["title"]=this["data-title"];this["data-title"]=""' onmouseleave='this["title"]=this["data-title"];this["data-title"]=""'
@ -44,7 +44,7 @@ var options = {
aspectRatio: "<%= aspect_ratio %>", aspectRatio: "<%= aspect_ratio %>",
<% end %> <% end %>
preload: "auto", 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: { controlBar: {
children: [ children: [
"playToggle", "playToggle",
@ -78,6 +78,7 @@ var player = videojs("player", options, function() {
volumeStep: 0.1, volumeStep: 0.1,
seekStep: 5, seekStep: 5,
enableModifiersForNumbers: false, enableModifiersForNumbers: false,
enableHoverScroll: true,
customKeys: { customKeys: {
// Toggle play with K Key // Toggle play with K Key
play: { play: {
@ -151,7 +152,7 @@ player.on('error', function(event) {
} }
player.currentTime(currentTime); player.currentTime(currentTime);
player.playbackRate(playbackRate); player.playbackRate(playbackRate);
if (!paused) { if (!paused) {
player.play(); player.play();
} }
@ -191,7 +192,7 @@ var bpb = player.getChild('bigPlayButton');
if (bpb) { if (bpb) {
bpb.hide(); bpb.hide();
player.ready(function() { player.ready(function() {
new Promise(function(resolve, reject) { new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1); setTimeout(() => resolve(1), 1);

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -33,7 +33,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -47,7 +47,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -61,7 +61,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -75,7 +75,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -89,7 +89,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -103,7 +103,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -117,7 +117,7 @@
</td> </td>
<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> </td>
</tr> </tr>
@ -131,7 +131,7 @@
</td> </td>
<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> </td>
</tr> </tr>

View File

@ -8,7 +8,7 @@
<div class="h-box"> <div class="h-box">
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-1-2"> <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") %> <%= translate(locale, "Login/Register") %>
</a> </a>
</div> </div>
@ -22,55 +22,84 @@
<% if account_type == "invidious" %> <% if account_type == "invidious" %>
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=invidious" method="post"> <form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=invidious" method="post">
<fieldset> <fieldset>
<% if email %>
<input name="email" type="hidden" value="<%= email %>">
<% else %>
<label for="email"><%= translate(locale, "User ID:") %></label> <label for="email"><%= translate(locale, "User ID:") %></label>
<input required class="pure-input-1" name="email" type="text" placeholder="User ID"> <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> <label for="password"><%= translate(locale, "Password:") %></label>
<input required class="pure-input-1" name="password" type="password" placeholder="Password"> <input required class="pure-input-1" name="password" type="password" placeholder="Password">
<% end %>
<% if config.captcha_enabled %> <% if captcha %>
<% if captcha_type == "image" %> <% case captcha_type when %>
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/> <% when "image" %>
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>"> <% captcha = captcha.not_nil! %>
<input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>"> <img style="width:100%" src='<%= captcha[:question] %>'/>
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label> <% captcha[:tokens].each_with_index do |token, i| %>
<input required type="text" name="answer" type="text" placeholder="h:mm:ss"> <input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>">
<input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>">
<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] %>">
<% end %> <% end %>
<label for="text_answer"><%= text_captcha.not_nil![:question] %></label> <input type="hidden" name="captcha_type" value="image">
<input required type="text" name="text_answer" type="text" placeholder="Answer"> <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> <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") %> <%= translate(locale, "Image CAPTCHA") %>
</a> </button>
</label> </label>
<% end %> <% end %>
<% else %>
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
<%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>
</button>
<% end %> <% end %>
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button>
<% if config.registration_enabled %>
<button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button>
<% end %>
</fieldset> </fieldset>
</form> </form>
<% elsif account_type == "google" %> <% 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> <fieldset>
<% if email %>
<input name="email" type="hidden" value="<%= email %>">
<% else %>
<label for="email"><%= translate(locale, "Email:") %></label> <label for="email"><%= translate(locale, "Email:") %></label>
<input required class="pure-input-1" name="email" type="email" placeholder="Email"> <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> <label for="password"><%= translate(locale, "Password:") %></label>
<input required class="pure-input-1" name="password" type="password" placeholder="Password"> <input required class="pure-input-1" name="password" type="password" placeholder="Password">
<% end %>
<% if tfa %> <% if tfa %>
<label for="tfa"><%= translate(locale, "Google verification code:") %></label> <label for="tfa"><%= translate(locale, "Google verification code:") %></label>

View File

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

View File

@ -31,7 +31,7 @@
<%= rendered "components/item" %> <%= rendered "components/item" %>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>
<div class="pure-g h-box"> <div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-1-5"> <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"> <div class="pure-control-group">
<label for="speed"><%= translate(locale, "Default speed: ") %></label> <label for="speed"><%= translate(locale, "Default speed: ") %></label>
<select name="speed" id="speed"> <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> <option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
<% end %> <% end %>
</select> </select>

View File

@ -5,15 +5,15 @@
<div class="h-box"> <div class="h-box">
<%= Markdown.to_html(<<-END_PRIVACY_POLICY <%= Markdown.to_html(<<-END_PRIVACY_POLICY
## Privacy ## 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. 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 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. 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: Information stored about a registered user is limited to:
- a list of session tokens for remaining logged in across devices - a list of session tokens for remaining logged in across devices
- the last time an account was updated (to provide accurate notifications) - the last time an account was updated (to provide accurate notifications)
- a list of video IDs identifying notifications from a user's subscriptions - 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 hashed password if applicable (not present on google accounts)
- a randomly generated token for providing an RSS feed of a user's subscriptions - a randomly generated token for providing an RSS feed of a user's subscriptions
- a list of video IDs identifying watched videos - 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). 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. 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. 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 ### 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. 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: Information about a request is limited to:
- the time the request was made - the time the request was made
- the status code of the response - the status code of the response
- the method of the request - the method of the request
- the requested URL - the requested URL
- how long it took to complete the request. - 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: 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: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: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 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 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). 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 ### Data stored in your browser
This website uses browser cookies to authenticate registered users. This data consists of: 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 - 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. 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. 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 ### 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 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. 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 END_PRIVACY_POLICY
) )

View File

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

View File

@ -181,50 +181,35 @@
<% end %> <% end %>
</div> </div>
</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>
<div class="content"> <div class="pure-u-1 pure-u-md-1-3">
<%= content %> <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>
<div class="footer"> <div class="pure-u-1 pure-u-md-1-3">
<div class="pure-g"> <i class="icon ion-logo-github"></i>
<div class="pure-u-1 pure-u-md-1-3"> <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %>
<a href="https://github.com/omarroth/invidious"> <i class="icon ion-logo-github"></i>
<%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %> <%= CURRENT_BRANCH %></div>
</a> </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="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> </div>
</div> </div>
<script src="/js/ui.js"></script> <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:url" content="<%= host_url %>/watch?v=<%= video.id %>">
<meta name="twitter:title" content="<%= HTML.escape(video.title) %>"> <meta name="twitter:title" content="<%= HTML.escape(video.title) %>">
<meta name="twitter:description" content="<%= description %>"> <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" content="<%= host_url %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280"> <meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720"> <meta name="twitter:player:height" content="720">
@ -33,7 +33,7 @@
<div class="h-box"> <div class="h-box">
<h1> <h1>
<%= HTML.escape(video.title) %> <%= HTML.escape(video.title) %>
<% if params[:listen] %> <% if params[:listen] %>
<a title="<%=translate(locale, "Video mode")%>" href="/watch?<%= env.params.query %>&listen=0"> <a title="<%=translate(locale, "Video mode")%>" href="/watch?<%= env.params.query %>&listen=0">
<i class="icon ion-ios-videocam"></i> <i class="icon ion-ios-videocam"></i>
@ -53,23 +53,23 @@
<div class="pure-u-1 pure-u-md-1-5"> <div class="pure-u-1 pure-u-md-1-5">
<div class="h-box"> <div class="h-box">
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p> <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"> <form class="pure-form pure-form-stacked" action="/latest_version" method="get" rel="noopener" target="_blank">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="download_widget"><%= translate(locale, "Download as: ") %></label> <label for="download_widget"><%= translate(locale, "Download as: ") %></label>
<select style="width:100%" name="download_widget" id="download_widget"> <select style="width:100%" name="download_widget" id="download_widget">
<% video_streams.each do |option| %> <% 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["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only
</option> </option>
<% end %> <% end %>
<% audio_streams.each do |option| %> <% 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["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only
</option> </option>
<% end %> <% end %>
<% fmt_stream.each do |option| %> <% 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] %> <%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %>
</option> </option>
<% end %> <% end %>
@ -150,7 +150,7 @@
<% if params[:related_videos] %> <% if params[:related_videos] %>
<div class="h-box"> <div class="h-box">
<% if !rvs.empty? %> <% if !rvs.empty? %>
<div id="continue" <% if plid %>style="display:none"<% end %>> <div id="continue" <% if plid %>style="display:none"<% end %>>
<div class="pure-control-group"> <div class="pure-control-group">
@ -187,7 +187,7 @@
<script> <script>
<% if !rvs.empty? && !plid && params[:continue] %> <% if !rvs.empty? && !plid && params[:continue] %>
player.on('ended', function() { player.on('ended', function() {
location.assign("/watch?v=" location.assign("/watch?v="
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>" + "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
+ "&continue=1" + "&continue=1"
<% if params[:listen] %> <% if params[:listen] %>
@ -206,7 +206,7 @@ player.on('ended', function() {
function continue_autoplay(target) { function continue_autoplay(target) {
if (target.checked) { if (target.checked) {
player.on('ended', function() { player.on('ended', function() {
location.assign("/watch?v=" location.assign("/watch?v="
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>" + "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
+ "&continue=1" + "&continue=1"
<% if params[:listen] %> <% if params[:listen] %>
@ -249,7 +249,7 @@ function get_playlist(timeouts = 0) {
} }
playlist.innerHTML = ' \ 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>' <hr>'
var plid = "<%= plid %>" var plid = "<%= plid %>"
@ -270,10 +270,10 @@ function get_playlist(timeouts = 0) {
if (xhr.readyState == 4) { if (xhr.readyState == 4) {
if (xhr.status == 200) { if (xhr.status == 200) {
playlist.innerHTML = xhr.response.playlistHtml; playlist.innerHTML = xhr.response.playlistHtml;
if (xhr.response.nextVideo) { if (xhr.response.nextVideo) {
player.on('ended', function() { player.on('ended', function() {
location.assign("/watch?v=" location.assign("/watch?v="
+ xhr.response.nextVideo + xhr.response.nextVideo
+ "&list=<%= plid %>" + "&list=<%= plid %>"
<% if params[:listen] %> <% if params[:listen] %>
@ -300,7 +300,7 @@ function get_playlist(timeouts = 0) {
comments = document.getElementById("playlist"); comments = document.getElementById("playlist");
comments.innerHTML = 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); get_playlist(timeouts + 1);
}; };
} }
@ -319,7 +319,7 @@ function get_reddit_comments(timeouts = 0) {
var fallback = comments.innerHTML; var fallback = comments.innerHTML;
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 url = "/api/v1/comments/<%= video.id %>?source=reddit&format=html&hl=<%= env.get("preferences").as(Preferences).locale %>";
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
@ -355,7 +355,7 @@ function get_reddit_comments(timeouts = 0) {
contentHtml: xhr.response.contentHtml contentHtml: xhr.response.contentHtml
}); });
} else { } else {
<% if preferences && preferences.comments[1] == "youtube" %> <% if preferences && preferences.comments[1] == "youtube" %>
get_youtube_comments(); get_youtube_comments();
<% else %> <% else %>
comments.innerHTML = fallback; comments.innerHTML = fallback;
@ -382,7 +382,7 @@ function get_youtube_comments(timeouts = 0) {
var fallback = comments.innerHTML; var fallback = comments.innerHTML;
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 url = "/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("preferences").as(Preferences).locale %>";
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
@ -416,7 +416,7 @@ function get_youtube_comments(timeouts = 0) {
comments.innerHTML = ""; comments.innerHTML = "";
} }
} else { } else {
<% if preferences && preferences.comments[1] == "youtube" %> <% if preferences && preferences.comments[1] == "youtube" %>
get_youtube_comments(); get_youtube_comments();
<% else %> <% else %>
comments.innerHTML = ""; comments.innerHTML = "";
@ -429,7 +429,7 @@ function get_youtube_comments(timeouts = 0) {
console.log("Pulling comments timed out."); console.log("Pulling comments timed out.");
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>';
get_youtube_comments(timeouts + 1); get_youtube_comments(timeouts + 1);
}; };
} }
@ -440,7 +440,7 @@ function get_youtube_replies(target, load_more) {
var body = target.parentNode.parentNode; var body = target.parentNode.parentNode;
var fallback = body.innerHTML; var fallback = body.innerHTML;
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=' + var url = '/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("preferences").as(Preferences).locale %>&continuation=' +
continuation; continuation;