mirror of
https://github.com/iv-org/invidious.git
synced 2025-07-30 09:18:28 +00:00
Merge remote-tracking branch 'upstream/master' into side-menu
This commit is contained in:
commit
31b587baea
@ -58,6 +58,7 @@ div {
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@ -80,11 +81,15 @@ a.pure-button-primary:hover {
|
||||
}
|
||||
|
||||
div.thumbnail {
|
||||
padding: 28.125%;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
@ -255,6 +260,41 @@ img.thumbnail {
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-play-control,
|
||||
.vjs-volume-panel,
|
||||
.vjs-current-time,
|
||||
.vjs-time-control,
|
||||
.vjs-duration,
|
||||
.vjs-progress-control,
|
||||
.vjs-remaining-time {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.vjs-captions-button {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.vjs-quality-selector {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.vjs-playback-rate {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.vjs-share-control {
|
||||
order: 5;
|
||||
}
|
||||
|
||||
.vjs-fullscreen-control {
|
||||
order: 6;
|
||||
}
|
||||
|
||||
.vjs-control-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.video-js .vjs-control-bar,
|
||||
.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
|
||||
background-color: rgba(35, 35, 35, 0.75);
|
||||
@ -326,29 +366,17 @@ img.thumbnail {
|
||||
padding-top: 82vh;
|
||||
}
|
||||
|
||||
video.video-js {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#player-container {
|
||||
position: relative;
|
||||
padding-bottom: 82vh;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#progress-container {
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
background-color: #a0a0a0;
|
||||
color: rgba(35, 35, 35, 1);
|
||||
}
|
||||
|
||||
#download-progress {
|
||||
width: 0%;
|
||||
border-radius: 2px;
|
||||
height: 10px;
|
||||
background-color: rgba(0, 182, 240, 1);
|
||||
color: #fff;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.pure-control-group label {
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
2
assets/css/video-js.min.css
vendored
2
assets/css/video-js.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* videojs-share
|
||||
* @version 2.0.1
|
||||
* @version 3.0.0
|
||||
* @copyright 2018 Mikhail Khazov <mkhazov.work@gmail.com>
|
||||
* @license MIT
|
||||
*/
|
||||
.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-modal-dialog-content{display:flex;align-items:center;padding:0;background-image:linear-gradient(to bottom, rgba(0,0,0,0.77), rgba(0,0,0,0.75))}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{position:absolute;right:0;top:5px;width:30px;height:30px;color:#fff;cursor:pointer;opacity:0.9;transition:opacity 0.25s ease-out}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:before{content:'×';font-size:20px;line-height:15px}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:hover{opacity:1}.video-js .vjs-share{display:flex;flex-direction:column;justify-content:space-around;align-items:center;width:100%;height:100%;max-height:400px}.video-js .vjs-share__top,.video-js .vjs-share__middle,.video-js .vjs-share__bottom{display:flex}.video-js .vjs-share__top,.video-js .vjs-share__middle{flex-direction:column;justify-content:space-between}.video-js .vjs-share__middle{padding:0 25px}.video-js .vjs-share__title{align-self:center;font-size:22px;color:#fff}.video-js .vjs-share__subtitle{width:100%;margin:0 auto 12px;font-size:16px;color:#fff;opacity:0.7}.video-js .vjs-share__short-link-wrapper{position:relative;display:block;width:100%;height:40px;margin:0 auto;margin-bottom:15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none;overflow:hidden;flex-shrink:0}.video-js .vjs-share__short-link{display:block;width:100%;height:100%;padding:0 40px 0 15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none}.video-js .vjs-share__btn{position:absolute;right:0;bottom:0;height:40px;width:40px;display:flex;align-items:center;padding:0 11px;border:0;color:#fff;background-color:#2e2e2e;background-size:18px 19px;background-position:center;background-repeat:no-repeat;cursor:pointer;outline:none;transition:width 0.3s ease-out, padding 0.3s ease-out}.video-js .vjs-share__btn svg{flex-shrink:0}.video-js .vjs-share__btn span{position:relative;padding-left:10px;opacity:0;transition:opacity 0.3s ease-out}.video-js .vjs-share__btn:hover{justify-content:center;width:100%;padding:0 40px;background-image:none}.video-js .vjs-share__btn:hover span{opacity:1}.video-js .vjs-share__socials{display:flex;flex-wrap:wrap;justify-content:center;align-content:flex-start;transition:width 0.3s ease-out, height 0.3s ease-out}.video-js .vjs-share__social{display:flex;justify-content:center;align-items:center;flex-shrink:0;width:32px;height:32px;margin-right:6px;margin-bottom:6px;cursor:pointer;font-size:8px;transition:transform 0.3s ease-out, filter 0.2s ease-out;border:none;outline:none}.video-js .vjs-share__social:hover{filter:brightness(115%)}.video-js .vjs-share__social svg{width:100%;max-height:24px}.video-js .vjs-share__social_vk{background-color:#5d7294}.video-js .vjs-share__social_ok{background-color:#ed7c20}.video-js .vjs-share__social_mail{background-color:#134785}.video-js .vjs-share__social_tw{background-color:#76aaeb}.video-js .vjs-share__social_reddit{background-color:#ff4500}.video-js .vjs-share__social_fbFeed{background-color:#475995}.video-js .vjs-share__social_messenger{background-color:#0084ff}.video-js .vjs-share__social_gp{background-color:#d53f35}.video-js .vjs-share__social_linkedin{background-color:#0077b5}.video-js .vjs-share__social_viber{background-color:#766db5}.video-js .vjs-share__social_telegram{background-color:#4bb0e2}.video-js .vjs-share__social_whatsapp{background-color:#78c870}.video-js .vjs-share__bottom{justify-content:center}@media (max-height: 220px){.video-js .vjs-share .hidden-xs{display:none}}@media (max-height: 350px){.video-js .vjs-share .hidden-sm{display:none}}@media (min-height: 400px){.video-js .vjs-share__title{margin-bottom:15px}.video-js .vjs-share__short-link-wrapper{margin-bottom:30px}}@media (min-width: 320px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:5px;top:10px}}@media (min-width: 660px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:20px;top:20px}.video-js .vjs-share__social{width:40px;height:40px}}
|
||||
.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-modal-dialog-content{display:flex;align-items:center;padding:0;background-image:linear-gradient(to bottom, rgba(0,0,0,0.77), rgba(0,0,0,0.75))}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{position:absolute;right:0;top:5px;width:30px;height:30px;color:#fff;cursor:pointer;opacity:0.9;transition:opacity 0.25s ease-out}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:before{content:'×';font-size:20px;line-height:15px}.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button:hover{opacity:1}.video-js .vjs-share{display:flex;flex-direction:column;justify-content:space-around;align-items:center;width:100%;height:100%;max-height:400px}.video-js .vjs-share__top,.video-js .vjs-share__middle,.video-js .vjs-share__bottom{display:flex}.video-js .vjs-share__top,.video-js .vjs-share__middle{flex-direction:column;justify-content:space-between}.video-js .vjs-share__middle{padding:0 25px}.video-js .vjs-share__title{align-self:center;font-size:22px;color:#fff}.video-js .vjs-share__subtitle{width:100%;margin:0 auto 12px;font-size:16px;color:#fff;opacity:0.7}.video-js .vjs-share__short-link-wrapper{position:relative;display:block;width:100%;height:40px;margin:0 auto;margin-bottom:15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none;overflow:hidden;flex-shrink:0}.video-js .vjs-share__short-link{display:block;width:100%;height:100%;padding:0 40px 0 15px;border:0;color:rgba(255,255,255,0.65);background-color:#363636;outline:none}.video-js .vjs-share__btn{position:absolute;right:0;bottom:0;height:40px;width:40px;display:flex;align-items:center;padding:0 11px;border:0;color:#fff;background-color:#2e2e2e;background-size:18px 19px;background-position:center;background-repeat:no-repeat;cursor:pointer;outline:none;transition:width 0.3s ease-out, padding 0.3s ease-out}.video-js .vjs-share__btn svg{flex-shrink:0}.video-js .vjs-share__btn span{position:relative;padding-left:10px;opacity:0;transition:opacity 0.3s ease-out}.video-js .vjs-share__btn:hover{justify-content:center;width:100%;padding:0 40px;background-image:none}.video-js .vjs-share__btn:hover span{opacity:1}.video-js .vjs-share__socials{display:flex;flex-wrap:wrap;justify-content:center;align-content:flex-start;transition:width 0.3s ease-out, height 0.3s ease-out}.video-js .vjs-share__social{display:flex;justify-content:center;align-items:center;flex-shrink:0;width:32px;height:32px;margin-right:6px;margin-bottom:6px;cursor:pointer;font-size:8px;transition:transform 0.3s ease-out, filter 0.2s ease-out;border:none;outline:none}.video-js .vjs-share__social:hover{filter:brightness(115%)}.video-js .vjs-share__social svg{overflow:visible;max-height:24px}.video-js .vjs-share__social_vk{background-color:#5d7294}.video-js .vjs-share__social_ok{background-color:#ed7c20}.video-js .vjs-share__social_mail{background-color:#134785}.video-js .vjs-share__social_tw{background-color:#76aaeb}.video-js .vjs-share__social_reddit{background-color:#ff4500}.video-js .vjs-share__social_fbFeed{background-color:#475995}.video-js .vjs-share__social_messenger{background-color:#0084ff}.video-js .vjs-share__social_gp{background-color:#d53f35}.video-js .vjs-share__social_linkedin{background-color:#0077b5}.video-js .vjs-share__social_viber{background-color:#766db5}.video-js .vjs-share__social_telegram{background-color:#4bb0e2}.video-js .vjs-share__social_whatsapp{background-color:#78c870}.video-js .vjs-share__bottom{justify-content:center}@media (max-height: 220px){.video-js .vjs-share .hidden-xs{display:none}}@media (max-height: 350px){.video-js .vjs-share .hidden-sm{display:none}}@media (min-height: 400px){.video-js .vjs-share__title{margin-bottom:15px}.video-js .vjs-share__short-link-wrapper{margin-bottom:30px}}@media (min-width: 320px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:5px;top:10px}}@media (min-width: 660px){.video-js.vjs-videojs-share_open .vjs-modal-dialog .vjs-close-button{right:20px;top:20px}.video-js .vjs-share__social{width:40px;height:40px}}
|
||||
|
19
assets/js/video.min.js
vendored
19
assets/js/video.min.js
vendored
File diff suppressed because one or more lines are too long
4
assets/js/videojs-share.min.js
vendored
4
assets/js/videojs-share.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,413 +0,0 @@
|
||||
/*
|
||||
* Video.js Hotkeys
|
||||
* https://github.com/ctd1500/videojs-hotkeys
|
||||
*
|
||||
* Copyright (c) 2015 Chris Dougherty
|
||||
* Licensed under the Apache-2.0 license.
|
||||
*/
|
||||
|
||||
;(function(root, factory) {
|
||||
if (typeof window !== 'undefined' && window.videojs) {
|
||||
factory(window.videojs);
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define('videojs-hotkeys', ['video.js'], function (module) {
|
||||
return factory(module.default || module);
|
||||
});
|
||||
} else if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = factory(require('video.js'));
|
||||
}
|
||||
}(this, function (videojs) {
|
||||
"use strict";
|
||||
if (typeof window !== 'undefined') {
|
||||
window['videojs_hotkeys'] = { version: "0.2.22" };
|
||||
}
|
||||
|
||||
var hotkeys = function(options) {
|
||||
var player = this;
|
||||
var pEl = player.el();
|
||||
var doc = document;
|
||||
var def_options = {
|
||||
volumeStep: 0.1,
|
||||
seekStep: 5,
|
||||
enableMute: true,
|
||||
enableVolumeScroll: true,
|
||||
enableHoverScroll: true,
|
||||
enableFullscreen: true,
|
||||
enableNumbers: true,
|
||||
enableJogStyle: false,
|
||||
alwaysCaptureHotkeys: false,
|
||||
enableModifiersForNumbers: true,
|
||||
enableInactiveFocus: true,
|
||||
skipInitialFocus: false,
|
||||
playPauseKey: playPauseKey,
|
||||
rewindKey: rewindKey,
|
||||
forwardKey: forwardKey,
|
||||
volumeUpKey: volumeUpKey,
|
||||
volumeDownKey: volumeDownKey,
|
||||
muteKey: muteKey,
|
||||
fullscreenKey: fullscreenKey,
|
||||
customKeys: {}
|
||||
};
|
||||
|
||||
var cPlay = 1,
|
||||
cRewind = 2,
|
||||
cForward = 3,
|
||||
cVolumeUp = 4,
|
||||
cVolumeDown = 5,
|
||||
cMute = 6,
|
||||
cFullscreen = 7;
|
||||
|
||||
// Use built-in merge function from Video.js v5.0+ or v4.4.0+
|
||||
var mergeOptions = videojs.mergeOptions || videojs.util.mergeOptions;
|
||||
options = mergeOptions(def_options, options || {});
|
||||
|
||||
var volumeStep = options.volumeStep,
|
||||
seekStep = options.seekStep,
|
||||
enableMute = options.enableMute,
|
||||
enableVolumeScroll = options.enableVolumeScroll,
|
||||
enableHoverScroll = options.enableHoverScroll,
|
||||
enableFull = options.enableFullscreen,
|
||||
enableNumbers = options.enableNumbers,
|
||||
enableJogStyle = options.enableJogStyle,
|
||||
alwaysCaptureHotkeys = options.alwaysCaptureHotkeys,
|
||||
enableModifiersForNumbers = options.enableModifiersForNumbers,
|
||||
enableInactiveFocus = options.enableInactiveFocus,
|
||||
skipInitialFocus = options.skipInitialFocus;
|
||||
|
||||
// Set default player tabindex to handle keydown and doubleclick events
|
||||
if (!pEl.hasAttribute('tabIndex')) {
|
||||
pEl.setAttribute('tabIndex', '-1');
|
||||
}
|
||||
|
||||
// Remove player outline to fix video performance issue
|
||||
pEl.style.outline = "none";
|
||||
|
||||
if (alwaysCaptureHotkeys || !player.autoplay()) {
|
||||
if (!skipInitialFocus) {
|
||||
player.one('play', function() {
|
||||
pEl.focus(); // Fixes the .vjs-big-play-button handing focus back to body instead of the player
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (enableInactiveFocus) {
|
||||
player.on('userinactive', function() {
|
||||
// When the control bar fades, re-apply focus to the player if last focus was a control button
|
||||
var cancelFocusingPlayer = function() {
|
||||
clearTimeout(focusingPlayerTimeout);
|
||||
};
|
||||
var focusingPlayerTimeout = setTimeout(function() {
|
||||
player.off('useractive', cancelFocusingPlayer);
|
||||
var activeElement = doc.activeElement;
|
||||
var controlBar = pEl.querySelector('.vjs-control-bar');
|
||||
if (activeElement && activeElement.parentElement == controlBar) {
|
||||
pEl.focus();
|
||||
}
|
||||
}, 10);
|
||||
|
||||
player.one('useractive', cancelFocusingPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
player.on('play', function() {
|
||||
// Fix allowing the YouTube plugin to have hotkey support.
|
||||
var ifblocker = pEl.querySelector('.iframeblocker');
|
||||
if (ifblocker && ifblocker.style.display === '') {
|
||||
ifblocker.style.display = "block";
|
||||
ifblocker.style.bottom = "39px";
|
||||
}
|
||||
});
|
||||
|
||||
var keyDown = function keyDown(event) {
|
||||
var ewhich = event.which, wasPlaying, seekTime;
|
||||
var ePreventDefault = event.preventDefault;
|
||||
var duration = player.duration();
|
||||
// When controls are disabled, hotkeys will be disabled as well
|
||||
if (player.controls()) {
|
||||
|
||||
// Don't catch keys if any control buttons are focused, unless alwaysCaptureHotkeys is true
|
||||
var activeEl = doc.activeElement;
|
||||
if (alwaysCaptureHotkeys ||
|
||||
activeEl == pEl ||
|
||||
activeEl == pEl.querySelector('.vjs-tech') ||
|
||||
activeEl == pEl.querySelector('.vjs-control-bar') ||
|
||||
activeEl == pEl.querySelector('.iframeblocker')) {
|
||||
|
||||
switch (checkKeys(event, player)) {
|
||||
// Spacebar toggles play/pause
|
||||
case cPlay:
|
||||
ePreventDefault();
|
||||
if (alwaysCaptureHotkeys) {
|
||||
// Prevent control activation with space
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (player.paused()) {
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
break;
|
||||
|
||||
// Seeking with the left/right arrow keys
|
||||
case cRewind: // Seek Backward
|
||||
wasPlaying = !player.paused();
|
||||
ePreventDefault();
|
||||
if (wasPlaying) {
|
||||
player.pause();
|
||||
}
|
||||
seekTime = player.currentTime() - seekStepD(event);
|
||||
// The flash player tech will allow you to seek into negative
|
||||
// numbers and break the seekbar, so try to prevent that.
|
||||
if (seekTime <= 0) {
|
||||
seekTime = 0;
|
||||
}
|
||||
player.currentTime(seekTime);
|
||||
if (wasPlaying) {
|
||||
player.play();
|
||||
}
|
||||
break;
|
||||
case cForward: // Seek Forward
|
||||
wasPlaying = !player.paused();
|
||||
ePreventDefault();
|
||||
if (wasPlaying) {
|
||||
player.pause();
|
||||
}
|
||||
seekTime = player.currentTime() + seekStepD(event);
|
||||
// Fixes the player not sending the end event if you
|
||||
// try to seek past the duration on the seekbar.
|
||||
if (seekTime >= duration) {
|
||||
seekTime = wasPlaying ? duration - .001 : duration;
|
||||
}
|
||||
player.currentTime(seekTime);
|
||||
if (wasPlaying) {
|
||||
player.play();
|
||||
}
|
||||
break;
|
||||
|
||||
// Volume control with the up/down arrow keys
|
||||
case cVolumeDown:
|
||||
ePreventDefault();
|
||||
if (!enableJogStyle) {
|
||||
player.volume(player.volume() - volumeStep);
|
||||
} else {
|
||||
seekTime = player.currentTime() - 1;
|
||||
if (player.currentTime() <= 1) {
|
||||
seekTime = 0;
|
||||
}
|
||||
player.currentTime(seekTime);
|
||||
}
|
||||
break;
|
||||
case cVolumeUp:
|
||||
ePreventDefault();
|
||||
if (!enableJogStyle) {
|
||||
player.volume(player.volume() + volumeStep);
|
||||
} else {
|
||||
seekTime = player.currentTime() + 1;
|
||||
if (seekTime >= duration) {
|
||||
seekTime = duration;
|
||||
}
|
||||
player.currentTime(seekTime);
|
||||
}
|
||||
break;
|
||||
|
||||
// Toggle Mute with the M key
|
||||
case cMute:
|
||||
if (enableMute) {
|
||||
player.muted(!player.muted());
|
||||
}
|
||||
break;
|
||||
|
||||
// Toggle Fullscreen with the F key
|
||||
case cFullscreen:
|
||||
if (enableFull) {
|
||||
if (player.isFullscreen()) {
|
||||
player.exitFullscreen();
|
||||
} else {
|
||||
player.requestFullscreen();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Number keys from 0-9 skip to a percentage of the video. 0 is 0% and 9 is 90%
|
||||
if ((ewhich > 47 && ewhich < 59) || (ewhich > 95 && ewhich < 106)) {
|
||||
// Do not handle if enableModifiersForNumbers set to false and keys are Ctrl, Cmd or Alt
|
||||
if (enableModifiersForNumbers || !(event.metaKey || event.ctrlKey || event.altKey)) {
|
||||
if (enableNumbers) {
|
||||
var sub = 48;
|
||||
if (ewhich > 95) {
|
||||
sub = 96;
|
||||
}
|
||||
var number = ewhich - sub;
|
||||
ePreventDefault();
|
||||
player.currentTime(player.duration() * number * 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle any custom hotkeys
|
||||
for (var customKey in options.customKeys) {
|
||||
var customHotkey = options.customKeys[customKey];
|
||||
// Check for well formed custom keys
|
||||
if (customHotkey && customHotkey.key && customHotkey.handler) {
|
||||
// Check if the custom key's condition matches
|
||||
if (customHotkey.key(event)) {
|
||||
ePreventDefault();
|
||||
customHotkey.handler(player, options, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var doubleClick = function doubleClick(event) {
|
||||
// When controls are disabled, hotkeys will be disabled as well
|
||||
if (player.controls()) {
|
||||
|
||||
// Don't catch clicks if any control buttons are focused
|
||||
var activeEl = event.relatedTarget || event.toElement || doc.activeElement;
|
||||
if (activeEl == pEl ||
|
||||
activeEl == pEl.querySelector('.vjs-tech') ||
|
||||
activeEl == pEl.querySelector('.iframeblocker')) {
|
||||
|
||||
if (enableFull) {
|
||||
if (player.isFullscreen()) {
|
||||
player.exitFullscreen();
|
||||
} else {
|
||||
player.requestFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var volumeHover = false;
|
||||
var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel');
|
||||
volumeSelector.onmouseover = function() { volumeHover = true; }
|
||||
volumeSelector.onmouseout = function() { volumeHover = false; }
|
||||
|
||||
var mouseScroll = function mouseScroll(event) {
|
||||
if (enableHoverScroll) {
|
||||
// If we leave this undefined then it can match non-existent elements below
|
||||
var activeEl = 0;
|
||||
} else {
|
||||
var activeEl = doc.activeElement;
|
||||
}
|
||||
|
||||
// When controls are disabled, hotkeys will be disabled as well
|
||||
if (player.controls()) {
|
||||
if (alwaysCaptureHotkeys ||
|
||||
activeEl == pEl ||
|
||||
activeEl == pEl.querySelector('.vjs-tech') ||
|
||||
activeEl == pEl.querySelector('.iframeblocker') ||
|
||||
activeEl == pEl.querySelector('.vjs-control-bar') ||
|
||||
volumeHover) {
|
||||
|
||||
if (enableVolumeScroll) {
|
||||
event = window.event || event;
|
||||
var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
|
||||
event.preventDefault();
|
||||
|
||||
if (delta == 1) {
|
||||
player.volume(player.volume() + volumeStep);
|
||||
} else if (delta == -1) {
|
||||
player.volume(player.volume() - volumeStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var checkKeys = function checkKeys(e, player) {
|
||||
// Allow some modularity in defining custom hotkeys
|
||||
|
||||
// Play/Pause check
|
||||
if (options.playPauseKey(e, player)) {
|
||||
return cPlay;
|
||||
}
|
||||
|
||||
// Seek Backward check
|
||||
if (options.rewindKey(e, player)) {
|
||||
return cRewind;
|
||||
}
|
||||
|
||||
// Seek Forward check
|
||||
if (options.forwardKey(e, player)) {
|
||||
return cForward;
|
||||
}
|
||||
|
||||
// Volume Up check
|
||||
if (options.volumeUpKey(e, player)) {
|
||||
return cVolumeUp;
|
||||
}
|
||||
|
||||
// Volume Down check
|
||||
if (options.volumeDownKey(e, player)) {
|
||||
return cVolumeDown;
|
||||
}
|
||||
|
||||
// Mute check
|
||||
if (options.muteKey(e, player)) {
|
||||
return cMute;
|
||||
}
|
||||
|
||||
// Fullscreen check
|
||||
if (options.fullscreenKey(e, player)) {
|
||||
return cFullscreen;
|
||||
}
|
||||
};
|
||||
|
||||
function playPauseKey(e) {
|
||||
// Space bar or MediaPlayPause
|
||||
return (e.which === 32 || e.which === 179);
|
||||
}
|
||||
|
||||
function rewindKey(e) {
|
||||
// Left Arrow or MediaRewind
|
||||
return (e.which === 37 || e.which === 177);
|
||||
}
|
||||
|
||||
function forwardKey(e) {
|
||||
// Right Arrow or MediaForward
|
||||
return (e.which === 39 || e.which === 176);
|
||||
}
|
||||
|
||||
function volumeUpKey(e) {
|
||||
// Up Arrow
|
||||
return (e.which === 38);
|
||||
}
|
||||
|
||||
function volumeDownKey(e) {
|
||||
// Down Arrow
|
||||
return (e.which === 40);
|
||||
}
|
||||
|
||||
function muteKey(e) {
|
||||
// M key
|
||||
return (e.which === 77);
|
||||
}
|
||||
|
||||
function fullscreenKey(e) {
|
||||
// F key
|
||||
return (e.which === 70);
|
||||
}
|
||||
|
||||
function seekStepD(e) {
|
||||
// SeekStep caller, returns an int, or a function returning an int
|
||||
return (typeof seekStep === "function" ? seekStep(e) : seekStep);
|
||||
}
|
||||
|
||||
player.on('keydown', keyDown);
|
||||
player.on('dblclick', doubleClick);
|
||||
player.on('mousewheel', mouseScroll);
|
||||
player.on("DOMMouseScroll", mouseScroll);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
var registerPlugin = videojs.registerPlugin || videojs.plugin;
|
||||
registerPlugin('hotkeys', hotkeys);
|
||||
}));
|
5
assets/js/videojs.hotkeys.min.js
vendored
5
assets/js/videojs.hotkeys.min.js
vendored
@ -1,2 +1,3 @@
|
||||
/* videojs-hotkeys v0.2.22 - https://github.com/ctd1500/videojs-hotkeys */
|
||||
!function(e,t){"undefined"!=typeof window&&window.videojs?t(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return t(e.default||e)}):"undefined"!=typeof module&&module.exports&&(module.exports=t(require("video.js")))}(0,function(s){"use strict";"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.22"});(s.registerPlugin||s.plugin)("hotkeys",function(m){var y=this,v=y.el(),f=document,e={volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!0,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},t=s.mergeOptions||s.util.mergeOptions,d=(m=t(e,m||{})).volumeStep,n=m.seekStep,p=m.enableMute,r=m.enableVolumeScroll,o=m.enableHoverScroll,b=m.enableFullscreen,h=m.enableNumbers,w=m.enableJogStyle,k=m.alwaysCaptureHotkeys,S=m.enableModifiersForNumbers,u=m.enableInactiveFocus,l=m.skipInitialFocus;v.hasAttribute("tabIndex")||v.setAttribute("tabIndex","-1"),v.style.outline="none",!k&&y.autoplay()||l||y.one("play",function(){v.focus()}),u&&y.on("userinactive",function(){var n=function(){clearTimeout(e)},e=setTimeout(function(){y.off("useractive",n);var e=f.activeElement,t=v.querySelector(".vjs-control-bar");e&&e.parentElement==t&&v.focus()},10);y.one("useractive",n)}),y.on("play",function(){var e=v.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")});var i=!1,c=v.querySelector(".vjs-volume-menu-button")||v.querySelector(".vjs-volume-panel");c.onmouseover=function(){i=!0},c.onmouseout=function(){i=!1};var a=function(e){if(o)var t=0;else t=f.activeElement;if(y.controls()&&(k||t==v||t==v.querySelector(".vjs-tech")||t==v.querySelector(".iframeblocker")||t==v.querySelector(".vjs-control-bar")||i)&&r){e=window.event||e;var n=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail));e.preventDefault(),1==n?y.volume(y.volume()+d):-1==n&&y.volume(y.volume()-d)}},K=function(e,t){return m.playPauseKey(e,t)?1:m.rewindKey(e,t)?2:m.forwardKey(e,t)?3:m.volumeUpKey(e,t)?4:m.volumeDownKey(e,t)?5:m.muteKey(e,t)?6:m.fullscreenKey(e,t)?7:void 0};function q(e){return"function"==typeof n?n(e):n}return y.on("keydown",function(e){var t,n,r=e.which,o=e.preventDefault,u=y.duration();if(y.controls()){var l=f.activeElement;if(k||l==v||l==v.querySelector(".vjs-tech")||l==v.querySelector(".vjs-control-bar")||l==v.querySelector(".iframeblocker"))switch(K(e,y)){case 1:o(),k&&e.stopPropagation(),y.paused()?y.play():y.pause();break;case 2:t=!y.paused(),o(),t&&y.pause(),(n=y.currentTime()-q(e))<=0&&(n=0),y.currentTime(n),t&&y.play();break;case 3:t=!y.paused(),o(),t&&y.pause(),u<=(n=y.currentTime()+q(e))&&(n=t?u-.001:u),y.currentTime(n),t&&y.play();break;case 5:o(),w?(n=y.currentTime()-1,y.currentTime()<=1&&(n=0),y.currentTime(n)):y.volume(y.volume()-d);break;case 4:o(),w?(u<=(n=y.currentTime()+1)&&(n=u),y.currentTime(n)):y.volume(y.volume()+d);break;case 6:p&&y.muted(!y.muted());break;case 7:b&&(y.isFullscreen()?y.exitFullscreen():y.requestFullscreen());break;default:if((47<r&&r<59||95<r&&r<106)&&(S||!(e.metaKey||e.ctrlKey||e.altKey))&&h){var i=48;95<r&&(i=96);var c=r-i;o(),y.currentTime(y.duration()*c*.1)}for(var a in m.customKeys){var s=m.customKeys[a];s&&s.key&&s.handler&&s.key(e)&&(o(),s.handler(y,m,e))}}}}),y.on("dblclick",function(e){if(y.controls()){var t=e.relatedTarget||e.toElement||f.activeElement;t!=v&&t!=v.querySelector(".vjs-tech")&&t!=v.querySelector(".iframeblocker")||b&&(y.isFullscreen()?y.exitFullscreen():y.requestFullscreen())}}),y.on("mousewheel",a),y.on("DOMMouseScroll",a),this})});
|
||||
/* videojs-hotkeys v0.2.25 - https://github.com/ctd1500/videojs-hotkeys */
|
||||
!function(e,n){"undefined"!=typeof window&&window.videojs?n(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return n(e.default||e)}):"undefined"!=typeof module&&module.exports&&(module.exports=n(require("video.js")))}(0,function(e){"use strict";"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.25"});(e.registerPlugin||e.plugin)("hotkeys",function(n){function t(e){return"function"==typeof s?s(e):s}function r(e){null!=e&&"function"==typeof e.then&&e.then(null,function(e){})}var o=this,u=o.el(),l=document,i={volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!1,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},c=e.mergeOptions||e.util.mergeOptions,a=(n=c(i,n||{})).volumeStep,s=n.seekStep,m=n.enableMute,f=n.enableVolumeScroll,y=n.enableHoverScroll,v=n.enableFullscreen,d=n.enableNumbers,p=n.enableJogStyle,b=n.alwaysCaptureHotkeys,h=n.enableModifiersForNumbers,w=n.enableInactiveFocus,k=n.skipInitialFocus,S=e.VERSION;u.hasAttribute("tabIndex")||u.setAttribute("tabIndex","-1"),u.style.outline="none",!b&&o.autoplay()||k||o.one("play",function(){u.focus()}),w&&o.on("userinactive",function(){var e=function(){clearTimeout(n)},n=setTimeout(function(){o.off("useractive",e);var n=l.activeElement,t=u.querySelector(".vjs-control-bar");n&&n.parentElement==t&&u.focus()},10);o.one("useractive",e)}),o.on("play",function(){var e=u.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")});var K=!1,q=u.querySelector(".vjs-volume-menu-button")||u.querySelector(".vjs-volume-panel");null!=q&&(q.onmouseover=function(){K=!0},q.onmouseout=function(){K=!1});var j=function(e){if(y)n=0;else var n=l.activeElement;if(o.controls()&&(b||n==u||n==u.querySelector(".vjs-tech")||n==u.querySelector(".iframeblocker")||n==u.querySelector(".vjs-control-bar")||K)&&f){e=window.event||e;var t=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail));e.preventDefault(),1==t?o.volume(o.volume()+a):-1==t&&o.volume(o.volume()-a)}},F=function(e,t){return n.playPauseKey(e,t)?1:n.rewindKey(e,t)?2:n.forwardKey(e,t)?3:n.volumeUpKey(e,t)?4:n.volumeDownKey(e,t)?5:n.muteKey(e,t)?6:n.fullscreenKey(e,t)?7:void 0};return o.on("keydown",function(e){var i,c,s=e.which,f=e.preventDefault,y=o.duration();if(o.controls()){var w=l.activeElement;if(b||w==u||w==u.querySelector(".vjs-tech")||w==u.querySelector(".vjs-control-bar")||w==u.querySelector(".iframeblocker"))switch(F(e,o)){case 1:f(),b&&e.stopPropagation(),o.paused()?r(o.play()):o.pause();break;case 2:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()-t(e))<=0&&(c=0),o.currentTime(c),i&&r(o.play());break;case 3:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()+t(e))>=y&&(c=i?y-.001:y),o.currentTime(c),i&&r(o.play());break;case 5:f(),p?(c=o.currentTime()-1,o.currentTime()<=1&&(c=0),o.currentTime(c)):o.volume(o.volume()-a);break;case 4:f(),p?((c=o.currentTime()+1)>=y&&(c=y),o.currentTime(c)):o.volume(o.volume()+a);break;case 6:m&&o.muted(!o.muted());break;case 7:v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen());break;default:if((s>47&&s<59||s>95&&s<106)&&(h||!(e.metaKey||e.ctrlKey||e.altKey))&&d){var k=48;s>95&&(k=96);var S=s-k;f(),o.currentTime(o.duration()*S*.1)}for(var K in n.customKeys){var q=n.customKeys[K];q&&q.key&&q.handler&&q.key(e)&&(f(),q.handler(o,n,e))}}}}),o.on("dblclick",function(e){if(null!=S&&S<="7.1.0"&&o.controls()){var n=e.relatedTarget||e.toElement||l.activeElement;n!=u&&n!=u.querySelector(".vjs-tech")&&n!=u.querySelector(".iframeblocker")||v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen())}}),o.on("mousewheel",j),o.on("DOMMouseScroll",j),this})});
|
||||
//# sourceMappingURL=videojs.hotkeys.min.js.map
|
@ -1,5 +1,3 @@
|
||||
video_threads: 0
|
||||
crawl_threads: 0
|
||||
channel_threads: 1
|
||||
feed_threads: 1
|
||||
db:
|
||||
|
7
config/migrate-scripts/migrate-db-1c8075c.sh
Executable file
7
config/migrate-scripts/migrate-db-1c8075c.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
psql invidious -c "ALTER TABLE channel_videos DROP COLUMN live_now CASCADE"
|
||||
psql invidious -c "ALTER TABLE channel_videos DROP COLUMN premiere_timestamp CASCADE"
|
||||
|
||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN live_now bool"
|
||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz"
|
4
config/migrate-scripts/migrate-db-6e51189.sh
Executable file
4
config/migrate-scripts/migrate-db-6e51189.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN live_now bool;"
|
||||
psql invidious -c "UPDATE channel_videos SET live_now = false;"
|
3
config/migrate-scripts/migrate-db-88b7097.sh
Executable file
3
config/migrate-scripts/migrate-db-88b7097.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
psql invidious -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz;"
|
@ -11,6 +11,8 @@ CREATE TABLE public.channel_videos
|
||||
ucid text,
|
||||
author text,
|
||||
length_seconds integer,
|
||||
live_now boolean,
|
||||
premiere_timestamp timestamp with time zone,
|
||||
CONSTRAINT channel_videos_id_key UNIQUE (id)
|
||||
);
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
"Clear watch history?": "Êtes-vous sûr de vouloir supprimer l'historique des vidéos regardées ?",
|
||||
"Yes": "Oui",
|
||||
"No": "Non",
|
||||
"Import and Export Data": "Importer et Exporter les Données",
|
||||
"Import and Export Data": "Importer et exporter des données",
|
||||
"Import": "Importer",
|
||||
"Import Invidious data": "Importer des données Invidious",
|
||||
"Import YouTube subscriptions": "Importer des abonnements YouTube",
|
||||
@ -45,19 +45,19 @@
|
||||
"Email:": "E-mail :",
|
||||
"Google verification code:": "Code de vérification Google :",
|
||||
"Preferences": "Préférences",
|
||||
"Player preferences": "Préférences du Lecteur",
|
||||
"Player preferences": "Préférences du lecteur",
|
||||
"Always loop: ": "Lire en boucle : ",
|
||||
"Autoplay: ": "Lire Automatiquement : ",
|
||||
"Autoplay: ": "Lire automatiquement : ",
|
||||
"Autoplay next video: ": "Lire automatiquement la vidéo suivante : ",
|
||||
"Listen by default: ": "Audio Uniquement par défaut : ",
|
||||
"Proxy videos? ": "Souhaitez vous charger les vidéos à travers un proxy ?",
|
||||
"Listen by default: ": "Audio uniquement : ",
|
||||
"Proxy videos? ": "Charger les vidéos à travers un proxy ? ",
|
||||
"Default speed: ": "Vitesse par défaut : ",
|
||||
"Preferred video quality: ": "Qualité vidéo souhaitée : ",
|
||||
"Player volume: ": "Volume du lecteur : ",
|
||||
"Default comments: ": "Source des Commentaires : ",
|
||||
"Default captions: ": "Sous-titres principal : ",
|
||||
"Fallback captions: ": "Sous-titres secondaire : ",
|
||||
"Show related videos? ": "Voir les vidéos liées à ce sujet ? ",
|
||||
"Default comments: ": "Source des commentaires : ",
|
||||
"Default captions: ": "Sous-titres par défaut : ",
|
||||
"Fallback captions: ": "Fallback captions: ",
|
||||
"Show related videos? ": "Voir les vidéos liées ? ",
|
||||
"Visual preferences": "Préférences du site",
|
||||
"Dark mode: ": "Mode Sombre : ",
|
||||
"Thin mode: ": "Mode Simplifié : ",
|
||||
@ -82,13 +82,13 @@
|
||||
"Watch history": "Historique de visionnage",
|
||||
"Delete account": "Supprimer votre compte",
|
||||
"Administrator preferences": "Préferences d'Administrateur",
|
||||
"Default homepage: ": "Page d'accueil par defaut :",
|
||||
"Feed menu: ": "Menu des Flux :",
|
||||
"Top enabled? ": "Top activé ?",
|
||||
"CAPTCHA enabled? ": "CAPTCHA activé ?",
|
||||
"Login enabled? ": "Connexion activé ?",
|
||||
"Registration enabled? ": "Inscription activé ?",
|
||||
"Report statistics? ": "Telemetrie activé ?",
|
||||
"Default homepage: ": "Page d'accueil par défaut : ",
|
||||
"Feed menu: ": "Menu des Flux : ",
|
||||
"Top enabled? ": "Top activé ? ",
|
||||
"CAPTCHA enabled? ": "CAPTCHA activé ? ",
|
||||
"Login enabled? ": "Connexion activé ? ",
|
||||
"Registration enabled? ": "Inscription activée ? ",
|
||||
"Report statistics? ": "Télémétrie activé ? ",
|
||||
"Save preferences": "Enregistrer les préférences",
|
||||
"Subscription manager": "Gestionnaire d'abonnement",
|
||||
"`x` subscriptions": "`x` abonnements",
|
||||
@ -108,11 +108,11 @@
|
||||
"License: ": "Licence : ",
|
||||
"Family friendly? ": "Tout Public ? ",
|
||||
"Wilson score: ": "Score de Wilson : ",
|
||||
"Engagement: ": "Poucentage de spectateur aillant aimé Liker ou Disliker la vidéo : ",
|
||||
"Engagement: ": "Poucentage de spectateur aillant aimé Like ou Dislike la vidéo : ",
|
||||
"Whitelisted regions: ": "Régions en liste blanche : ",
|
||||
"Blacklisted regions: ": "Régions sur liste noire : ",
|
||||
"Shared `x`": "Partagée `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Il semblerait que JavaScript sois désactivé. Cliquez ici pour voir les commentaires. Gardez à l'esprit que le chargement peut prendre plus de temps.",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Il semblerait que JavaScript soit désactivé. Cliquez ici pour voir les commentaires sans. Gardez à l'esprit que le chargement peut prendre plus de temps.",
|
||||
"View YouTube comments": "Voir les commentaires YouTube",
|
||||
"View more comments on Reddit": "Voir plus de commentaires sur Reddit",
|
||||
"View `x` comments": "Voir `x` commentaires",
|
||||
@ -124,11 +124,11 @@
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Si vous ne parvenez pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.",
|
||||
"Invalid TFA code": "Code d'authentification à deux facteurs invalide",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.",
|
||||
"Invalid answer": "Réponse non valide",
|
||||
"Invalid answer": "Réponse invalide",
|
||||
"Invalid CAPTCHA": "CAPTCHA invalide",
|
||||
"CAPTCHA is a required field": "Veuillez rentrez un CAPTCHA",
|
||||
"User ID is a required field": "Veuillez rentrez un Identifiant Utilisateur",
|
||||
"Password is a required field": "Veuillez rentrez un Mot de passe",
|
||||
"CAPTCHA is a required field": "Veuillez entrer un CAPTCHA",
|
||||
"User ID is a required field": "Veuillez entrer un Identifiant Utilisateur",
|
||||
"Password is a required field": "Veuillez entrer un Mot de passe",
|
||||
"Invalid username or password": "Nom d'utilisateur ou mot de passe invalide",
|
||||
"Please sign in using 'Sign in with Google'": "Veuillez vous connecter en utilisant \"S'identifier avec Google\"",
|
||||
"Password cannot be empty": "Le mot de passe ne peut pas être vide",
|
||||
@ -268,7 +268,7 @@
|
||||
"`x` hours": "`x` heures",
|
||||
"`x` minutes": "`x` minutes",
|
||||
"`x` seconds": "`x` secondes",
|
||||
"Fallback comments: ": "Commentaires secondaires : ",
|
||||
"Fallback comments: ": "Fallback comments: ",
|
||||
"Popular": "Populaire",
|
||||
"Top": "Top",
|
||||
"About": "A Propos",
|
||||
@ -289,5 +289,5 @@
|
||||
"Video mode": "Mode Vidéo",
|
||||
"Videos": "Vidéos",
|
||||
"Playlists": "Liste de lecture",
|
||||
"Current version: ": "Version actuelle :"
|
||||
"Current version: ": "Version :"
|
||||
}
|
||||
|
582
locales/pl.json
582
locales/pl.json
@ -1,293 +1,293 @@
|
||||
{
|
||||
"`x` subscribers": "`x` subskrybcji",
|
||||
"`x` videos": "`x` filmów",
|
||||
"LIVE": "NA ŻYWO",
|
||||
"Shared `x` ago": "Udostępniono `x` temu",
|
||||
"Unsubscribe": "Odsubskrybuj",
|
||||
"Subscribe": "Subskrybuj",
|
||||
"Login to subscribe to `x`": "Zaloguj się, aby subskrybować `x`",
|
||||
"View channel on YouTube": "Wyświetl kanał na YouTube",
|
||||
"newest": "najnowsze",
|
||||
"oldest": "najstarsze",
|
||||
"popular": "popularne",
|
||||
"last": "",
|
||||
"Next page": "Następna strona",
|
||||
"Previous page": "Poprzednia strona",
|
||||
"Clear watch history?": "Wyczyścić historię?",
|
||||
"Yes": "Tak",
|
||||
"No": "Nie",
|
||||
"Import and Export Data": "Import i eksport danych",
|
||||
"Import": "Import",
|
||||
"Import Invidious data": "Importuj dane Invidious",
|
||||
"Import YouTube subscriptions": "Importuj subskrybcje z YouTube",
|
||||
"Import FreeTube subscriptions (.db)": "Importuj subskrybcje z FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importuj subskrybcje z NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Importuj dane NewPipe (.zip)",
|
||||
"Export": "Eksport",
|
||||
"Export subscriptions as OPML": "Eksportuj subskrybcje jako OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrybcje jako OPML (dla NewPipe i FreeTube)",
|
||||
"Export data as JSON": "Eksportuj dane jako JSON",
|
||||
"Delete account?": "Usunąć konto?",
|
||||
"History": "Historia",
|
||||
"An alternative front-end to YouTube": "Alternatywny front-end dla YouTube",
|
||||
"JavaScript license information": "Informacja o licencji JavaScript",
|
||||
"source": "źródło",
|
||||
"Login": "Zaloguj",
|
||||
"Login/Register": "Zaloguj/Zarejestruj",
|
||||
"Login to Google": "Zaloguj do Google",
|
||||
"User ID:": "ID użytkownika:",
|
||||
"Password:": "Hasło:",
|
||||
"Time (h:mm:ss):": "Godzina (h:mm:ss):",
|
||||
"Text CAPTCHA": "Tekst CAPTCHA",
|
||||
"Image CAPTCHA": "Obraz CAPTCHA",
|
||||
"Sign In": "Zaloguj się",
|
||||
"Register": "Zarejestruj się",
|
||||
"Email:": "Email:",
|
||||
"Google verification code:": "Kod weryfikacyjny Google:",
|
||||
"Preferences": "Preferencje",
|
||||
"Player preferences": "Ustawienia odtwarzacza",
|
||||
"Always loop: ": "Zawsze zapętlaj: ",
|
||||
"Autoplay: ": "Autoodtwarzanie: ",
|
||||
"Autoplay next video: ": "Odtwórz następny film: ",
|
||||
"Listen by default: ": "Tryb dźwiękowy: ",
|
||||
"Proxy videos? ": "",
|
||||
"Default speed: ": "Domyślna prędkość: ",
|
||||
"Preferred video quality: ": "Preferowana jakość filmów: ",
|
||||
"Player volume: ": "Głośność odtwarzacza: ",
|
||||
"Default comments: ": "Domyślne komentarze: ",
|
||||
"Default captions: ": "Domyślne napisy: ",
|
||||
"Fallback captions: ": "Zastępcze napisy: ",
|
||||
"Show related videos? ": "Pokaż powiązane filmy? ",
|
||||
"Visual preferences": "Preferencje Wizualne",
|
||||
"Dark mode: ": "Ciemny motyw: ",
|
||||
"Thin mode: ": "Tryb minimalny: ",
|
||||
"Subscription preferences": "Preferencje subskrybcji",
|
||||
"Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
|
||||
"Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ",
|
||||
"Sort videos by: ": "Sortuj filmy: ",
|
||||
"published": "po czasie publikacji",
|
||||
"published - reverse": "po czasie publikacji od najstarszych",
|
||||
"alphabetically": "alfabetycznie",
|
||||
"alphabetically - reverse": "alfabetycznie od tyłu",
|
||||
"channel name": "po nazwie kanału",
|
||||
"channel name - reverse": "po nazwie kanału od tyłu",
|
||||
"Only show latest video from channel: ": "Pokazuj tylko najnowszy film z kanału: ",
|
||||
"Only show latest unwatched video from channel: ": "Pokazuj tylko najnowszy nie obejrzany film z kanału: ",
|
||||
"Only show unwatched: ": "Pokazuj tylko nie obejrzane: ",
|
||||
"Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ",
|
||||
"Data preferences": "Preferencje danych",
|
||||
"Clear watch history": "Wyczyść historię",
|
||||
"Import/Export data": "Import/Eksport danych",
|
||||
"Manage subscriptions": "Organizuj subskrybcje",
|
||||
"Watch history": "Historia",
|
||||
"Delete account": "Usuń konto",
|
||||
"Administrator preferences": "Preferencje administratora",
|
||||
"Default homepage: ": "Domyślna strona główna: ",
|
||||
"Feed menu: ": "",
|
||||
"Top enabled? ": "",
|
||||
"CAPTCHA enabled? ": "CAPTCHA aktywna? ",
|
||||
"Login enabled? ": "Logowanie włączone? ",
|
||||
"Registration enabled? ": "Rejestracja włączona? ",
|
||||
"Report statistics? ": "Raportować statystyki? ",
|
||||
"Save preferences": "Zapisz preferencje",
|
||||
"Subscription manager": "Manager subskrybcji",
|
||||
"`x` subscriptions": "`x` subskrybcji",
|
||||
"Import/Export": "Import/Eksport",
|
||||
"unsubscribe": "odsubskrybuj",
|
||||
"Subscriptions": "Subskrybcje",
|
||||
"`x` unseen notifications": "`x` niewidzianych powiadomień",
|
||||
"search": "szukaj",
|
||||
"Sign out": "Wyloguj",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Wydano na licencji AGPLv3 przez Omar Roth.",
|
||||
"Source available here.": "Kod źródłowy dostępny tutaj.",
|
||||
"View JavaScript license information.": "Wyświetl informację o licencji JavaScript.",
|
||||
"View privacy policy.": "",
|
||||
"Trending": "Na czasie",
|
||||
"Watch video on Youtube": "Zobacz film na YouTube",
|
||||
"Genre: ": "Gatunek: ",
|
||||
"License: ": "Licencja: ",
|
||||
"Family friendly? ": "Przyjazny rodzinie? ",
|
||||
"Wilson score: ": "Punktacja Wilsona: ",
|
||||
"Engagement: ": "Zaangażowanie: ",
|
||||
"Whitelisted regions: ": "Dostępny na obszarach: ",
|
||||
"Blacklisted regions: ": "Niedostępny na obszarach: ",
|
||||
"Shared `x`": "Udostępniono `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.",
|
||||
"View YouTube comments": "Wyświetl komentarze z YouTube",
|
||||
"View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
|
||||
"View `x` comments": "Wyświetl `x` komentarzy",
|
||||
"View Reddit comments": "Wyświetl komentarze z Redditta",
|
||||
"Hide replies": "Ukryj odpowiedzi",
|
||||
"Show replies": "Pokaż odpowiedzi",
|
||||
"Incorrect password": "Niepoprawne hasło",
|
||||
"Quota exceeded, try again in a few hours": "Przekroczony limit zapytań, spróbuj ponownie za kilka godzin",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Nie udało się zalogować, upewnij się, że dwuetapowe uwierzytelnianie (Autentykator lub SMS) jest aktywne.",
|
||||
"Invalid TFA code": "Niepoprawny kod TFA",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Nie udało się zalogować. To może być spowodowane wyłączoną dwustopniową autoryzacją na twoim koncie.",
|
||||
"Invalid answer": "Niepoprawna odpowiedź",
|
||||
"Invalid CAPTCHA": "CAPTCHA wykonane błędnie",
|
||||
"CAPTCHA is a required field": "CAPTCHA jest polem wymaganym",
|
||||
"User ID is a required field": "ID użytkownika jest polem wymaganym",
|
||||
"Password is a required field": "Hasło jest polem wymaganym",
|
||||
"Invalid username or password": "Niepoprawny login lub hasło",
|
||||
"Please sign in using 'Sign in with Google'": "Zaloguj się używając \"Zaloguj się przez Google\"",
|
||||
"Password cannot be empty": "Hasło nie może być puste",
|
||||
"Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków",
|
||||
"Please sign in": "Proszę się zalogować",
|
||||
"Invidious Private Feed for `x`": "",
|
||||
"channel:`x`": "kanał:`x",
|
||||
"Deleted or invalid channel": "Usunięty lub niepoprawny kanał",
|
||||
"This channel does not exist.": "Ten kanał nie istnieje.",
|
||||
"Could not get channel info.": "Nie udało się uzyskać informacji o kanale.",
|
||||
"Could not fetch comments": "Nie udało się pobrać komentarzy",
|
||||
"View `x` replies": "Wyświetl `x` odpowiedzi",
|
||||
"`x` ago": "`x` temu",
|
||||
"Load more": "Wczytaj więcej",
|
||||
"`x` points": "`x` punktów",
|
||||
"Could not create mix.": "Nie udało się utworzyć miksu.",
|
||||
"Playlist is empty": "Lista odtwarzania jest pusta",
|
||||
"Invalid playlist.": "Niepoprawna lista.",
|
||||
"Playlist does not exist.": "Lista odtwarzania nie istnieje.",
|
||||
"Could not pull trending pages.": "Nie udało się pobrać strony na czasie.",
|
||||
"Hidden field \"challenge\" is a required field": "Ukryte pole \"wyzwanie\" jest polem wymaganym",
|
||||
"Hidden field \"token\" is a required field": "Ukryte pole \"token\" jest polem wymaganym",
|
||||
"Invalid challenge": "Niepoprawne wyzwanie",
|
||||
"Invalid token": "Niepoprawny token",
|
||||
"Invalid user": "Niepoprawny użytkownik",
|
||||
"Token is expired, please try again": "Token wygasł, spróbuj ponownie",
|
||||
"English": "angielski",
|
||||
"English (auto-generated)": "angielski (automatycznie generowane)",
|
||||
"Afrikaans": "afrykanerski",
|
||||
"Albanian": "albański",
|
||||
"Amharic": "amharski",
|
||||
"Arabic": "arabski",
|
||||
"Armenian": "armeński",
|
||||
"Azerbaijani": "azerski",
|
||||
"Bangla": "bengalski",
|
||||
"Basque": "baskijski",
|
||||
"Belarusian": "białoruski",
|
||||
"Bosnian": "bośniacki",
|
||||
"Bulgarian": "bułgarski",
|
||||
"Burmese": "birmański",
|
||||
"Catalan": "kataloński",
|
||||
"Cebuano": "cebuański",
|
||||
"Chinese (Simplified)": "chiński (uproszczony)",
|
||||
"Chinese (Traditional)": "chiński (tradycyjny)",
|
||||
"Corsican": "korsykański",
|
||||
"Croatian": "chorwacki",
|
||||
"Czech": "czeski",
|
||||
"Danish": "duński",
|
||||
"Dutch": "holenderski",
|
||||
"Esperanto": "esperanto",
|
||||
"Estonian": "estoński",
|
||||
"Filipino": "filipiński",
|
||||
"Finnish": "fiński",
|
||||
"French": "francuski",
|
||||
"Galician": "galicyjski",
|
||||
"Georgian": "gruziński",
|
||||
"German": "niemiecki",
|
||||
"Greek": "grecki",
|
||||
"Gujarati": "gudźarati",
|
||||
"Haitian Creole": "kreolski haitański",
|
||||
"Hausa": "hausa",
|
||||
"Hawaiian": "hawajski",
|
||||
"Hebrew": "hebrajski",
|
||||
"Hindi": "hindi",
|
||||
"Hmong": "hmong",
|
||||
"Hungarian": "węgierski",
|
||||
"Icelandic": "islandzki",
|
||||
"Igbo": "ibo",
|
||||
"Indonesian": "indonezyjski",
|
||||
"Irish": "irlandzki",
|
||||
"Italian": "włoski",
|
||||
"Japanese": "japoński",
|
||||
"Javanese": "jawajski",
|
||||
"Kannada": "kannada",
|
||||
"Kazakh": "kazachski",
|
||||
"Khmer": "khmerski",
|
||||
"Korean": "koreański",
|
||||
"Kurdish": "kurdyjski",
|
||||
"Kyrgyz": "kirgiski",
|
||||
"Lao": "laotański",
|
||||
"Latin": "łaciński",
|
||||
"Latvian": "łotewski",
|
||||
"Lithuanian": "litewski",
|
||||
"Luxembourgish": "luksemburski",
|
||||
"Macedonian": "macedoński",
|
||||
"Malagasy": "malgaski",
|
||||
"Malay": "malajski",
|
||||
"Malayalam": "malajalam",
|
||||
"Maltese": "maltański",
|
||||
"Maori": "maoryski",
|
||||
"Marathi": "marathi",
|
||||
"Mongolian": "mongolski",
|
||||
"Nepali": "nepalski",
|
||||
"Norwegian": "norweski",
|
||||
"Nyanja": "njandża",
|
||||
"Pashto": "paszto",
|
||||
"Persian": "perski",
|
||||
"Polish": "polski",
|
||||
"Portuguese": "portugalski",
|
||||
"Punjabi": "pendżabski",
|
||||
"Romanian": "rumuński",
|
||||
"Russian": "rosyjski",
|
||||
"Samoan": "samoański",
|
||||
"Scottish Gaelic": "gaelicki szkocki",
|
||||
"Serbian": "serbski",
|
||||
"Shona": "shona",
|
||||
"Sindhi": "sindhi",
|
||||
"Sinhala": "syngaleski",
|
||||
"Slovak": "słowacki",
|
||||
"Slovenian": "słoweński",
|
||||
"Somali": "somalijski",
|
||||
"Southern Sotho": "sotho południowy",
|
||||
"Spanish": "hiszpański",
|
||||
"Spanish (Latin America)": "hiszpański (ameryka łacińska)",
|
||||
"Sundanese": "sundajski",
|
||||
"Swahili": "suahili",
|
||||
"Swedish": "szwedzki",
|
||||
"Tajik": "tadżycki",
|
||||
"Tamil": "tamilski",
|
||||
"Telugu": "telugu",
|
||||
"Thai": "tajski",
|
||||
"Turkish": "turecki",
|
||||
"Ukrainian": "ukraiński",
|
||||
"Urdu": "urdu",
|
||||
"Uzbek": "uzbecki",
|
||||
"Vietnamese": "wietnamski",
|
||||
"Welsh": "walijski",
|
||||
"Western Frisian": "zachodniofryzyjski",
|
||||
"Xhosa": "xhosa",
|
||||
"Yiddish": "jidysz",
|
||||
"Yoruba": "joruba",
|
||||
"Zulu": "zuluski",
|
||||
"`x` years": "`x` lat",
|
||||
"`x` months": "`x` miesięcy",
|
||||
"`x` weeks": "`x` tygodni",
|
||||
"`x` days": "`x` dni",
|
||||
"`x` hours": "`x` godzin",
|
||||
"`x` minutes": "`x` minut",
|
||||
"`x` seconds": "`x` sekund",
|
||||
"Fallback comments: ": "Zastępcze komentarze: ",
|
||||
"Popular": "Popularne",
|
||||
"Top": "Na czasie",
|
||||
"About": "Informacje",
|
||||
"Rating: ": "Ocena: ",
|
||||
"Language: ": "Język: ",
|
||||
"Default": "Domyślnie",
|
||||
"Music": "Muzyka",
|
||||
"Gaming": "Gry",
|
||||
"News": "Wiadomości",
|
||||
"Movies": "Filmy",
|
||||
"Download": "Pobierz",
|
||||
"Download as: ": "Pobierz jako: ",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "(edytowany)",
|
||||
"Youtube permalink of the comment": "Odnośnik bezpośredni do komentarza na YouTube",
|
||||
"`x` marked it with a ❤": "'x' oznaczonych ❤",
|
||||
"Audio mode": "Tryb audio",
|
||||
"Video mode": "Tryb wideo",
|
||||
"Videos": "Filmy",
|
||||
"Playlists": "Playlisty",
|
||||
"Current version: ": "Aktualna wersja: "
|
||||
"`x` subscribers": "`x` subskrybcji",
|
||||
"`x` videos": "`x` filmów",
|
||||
"LIVE": "NA ŻYWO",
|
||||
"Shared `x` ago": "Udostępniono `x` temu",
|
||||
"Unsubscribe": "Odsubskrybuj",
|
||||
"Subscribe": "Subskrybuj",
|
||||
"Login to subscribe to `x`": "Zaloguj się, aby subskrybować `x`",
|
||||
"View channel on YouTube": "Wyświetl kanał na YouTube",
|
||||
"newest": "najnowsze",
|
||||
"oldest": "najstarsze",
|
||||
"popular": "popularne",
|
||||
"last": "ostatnie",
|
||||
"Next page": "Następna strona",
|
||||
"Previous page": "Poprzednia strona",
|
||||
"Clear watch history?": "Wyczyścić historię?",
|
||||
"Yes": "Tak",
|
||||
"No": "Nie",
|
||||
"Import and Export Data": "Import i eksport danych",
|
||||
"Import": "Import",
|
||||
"Import Invidious data": "Importuj dane Invidious",
|
||||
"Import YouTube subscriptions": "Importuj subskrybcje z YouTube",
|
||||
"Import FreeTube subscriptions (.db)": "Importuj subskrybcje z FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importuj subskrybcje z NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Importuj dane NewPipe (.zip)",
|
||||
"Export": "Eksport",
|
||||
"Export subscriptions as OPML": "Eksportuj subskrybcje jako OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrybcje jako OPML (dla NewPipe i FreeTube)",
|
||||
"Export data as JSON": "Eksportuj dane jako JSON",
|
||||
"Delete account?": "Usunąć konto?",
|
||||
"History": "Historia",
|
||||
"An alternative front-end to YouTube": "Alternatywny front-end dla YouTube",
|
||||
"JavaScript license information": "Informacja o licencji JavaScript",
|
||||
"source": "źródło",
|
||||
"Login": "Zaloguj",
|
||||
"Login/Register": "Zaloguj/Zarejestruj",
|
||||
"Login to Google": "Zaloguj do Google",
|
||||
"User ID:": "ID użytkownika:",
|
||||
"Password:": "Hasło:",
|
||||
"Time (h:mm:ss):": "Godzina (h:mm:ss):",
|
||||
"Text CAPTCHA": "Tekst CAPTCHA",
|
||||
"Image CAPTCHA": "Obraz CAPTCHA",
|
||||
"Sign In": "Zaloguj się",
|
||||
"Register": "Zarejestruj się",
|
||||
"Email:": "Email:",
|
||||
"Google verification code:": "Kod weryfikacyjny Google:",
|
||||
"Preferences": "Preferencje",
|
||||
"Player preferences": "Ustawienia odtwarzacza",
|
||||
"Always loop: ": "Zawsze zapętlaj: ",
|
||||
"Autoplay: ": "Autoodtwarzanie: ",
|
||||
"Autoplay next video: ": "Odtwórz następny film: ",
|
||||
"Listen by default: ": "Tryb dźwiękowy: ",
|
||||
"Proxy videos? ": "Filmy przez proxy? ",
|
||||
"Default speed: ": "Domyślna prędkość: ",
|
||||
"Preferred video quality: ": "Preferowana jakość filmów: ",
|
||||
"Player volume: ": "Głośność odtwarzacza: ",
|
||||
"Default comments: ": "Domyślne komentarze: ",
|
||||
"Default captions: ": "Domyślne napisy: ",
|
||||
"Fallback captions: ": "Zastępcze napisy: ",
|
||||
"Show related videos? ": "Pokaż powiązane filmy? ",
|
||||
"Visual preferences": "Preferencje Wizualne",
|
||||
"Dark mode: ": "Ciemny motyw: ",
|
||||
"Thin mode: ": "Tryb minimalny: ",
|
||||
"Subscription preferences": "Preferencje subskrybcji",
|
||||
"Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
|
||||
"Number of videos shown in feed: ": "Liczba filmów widoczna na stronie subskrybcji: ",
|
||||
"Sort videos by: ": "Sortuj filmy: ",
|
||||
"published": "po czasie publikacji",
|
||||
"published - reverse": "po czasie publikacji od najstarszych",
|
||||
"alphabetically": "alfabetycznie",
|
||||
"alphabetically - reverse": "alfabetycznie od tyłu",
|
||||
"channel name": "po nazwie kanału",
|
||||
"channel name - reverse": "po nazwie kanału od tyłu",
|
||||
"Only show latest video from channel: ": "Pokazuj tylko najnowszy film z kanału: ",
|
||||
"Only show latest unwatched video from channel: ": "Pokazuj tylko najnowszy nie obejrzany film z kanału: ",
|
||||
"Only show unwatched: ": "Pokazuj tylko nie obejrzane: ",
|
||||
"Only show notifications (if there are any): ": "Pokazuj tylko powiadomienia (jeśli są): ",
|
||||
"Data preferences": "Preferencje danych",
|
||||
"Clear watch history": "Wyczyść historię",
|
||||
"Import/Export data": "Import/Eksport danych",
|
||||
"Manage subscriptions": "Organizuj subskrybcje",
|
||||
"Watch history": "Historia",
|
||||
"Delete account": "Usuń konto",
|
||||
"Administrator preferences": "Preferencje administratora",
|
||||
"Default homepage: ": "Domyślna strona główna: ",
|
||||
"Feed menu: ": "",
|
||||
"Top enabled? ": "",
|
||||
"CAPTCHA enabled? ": "CAPTCHA aktywna? ",
|
||||
"Login enabled? ": "Logowanie włączone? ",
|
||||
"Registration enabled? ": "Rejestracja włączona? ",
|
||||
"Report statistics? ": "Raportować statystyki? ",
|
||||
"Save preferences": "Zapisz preferencje",
|
||||
"Subscription manager": "Manager subskrybcji",
|
||||
"`x` subscriptions": "`x` subskrybcji",
|
||||
"Import/Export": "Import/Eksport",
|
||||
"unsubscribe": "odsubskrybuj",
|
||||
"Subscriptions": "Subskrybcje",
|
||||
"`x` unseen notifications": "`x` niewidzianych powiadomień",
|
||||
"search": "szukaj",
|
||||
"Sign out": "Wyloguj",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Wydano na licencji AGPLv3 przez Omar Roth.",
|
||||
"Source available here.": "Kod źródłowy dostępny tutaj.",
|
||||
"View JavaScript license information.": "Wyświetl informację o licencji JavaScript.",
|
||||
"View privacy policy.": "Polityka prywatności.",
|
||||
"Trending": "Na czasie",
|
||||
"Watch video on Youtube": "Zobacz film na YouTube",
|
||||
"Genre: ": "Gatunek: ",
|
||||
"License: ": "Licencja: ",
|
||||
"Family friendly? ": "Przyjazny rodzinie? ",
|
||||
"Wilson score: ": "Punktacja Wilsona: ",
|
||||
"Engagement: ": "Zaangażowanie: ",
|
||||
"Whitelisted regions: ": "Dostępny na obszarach: ",
|
||||
"Blacklisted regions: ": "Niedostępny na obszarach: ",
|
||||
"Shared `x`": "Udostępniono `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.",
|
||||
"View YouTube comments": "Wyświetl komentarze z YouTube",
|
||||
"View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
|
||||
"View `x` comments": "Wyświetl `x` komentarzy",
|
||||
"View Reddit comments": "Wyświetl komentarze z Redditta",
|
||||
"Hide replies": "Ukryj odpowiedzi",
|
||||
"Show replies": "Pokaż odpowiedzi",
|
||||
"Incorrect password": "Niepoprawne hasło",
|
||||
"Quota exceeded, try again in a few hours": "Przekroczony limit zapytań, spróbuj ponownie za kilka godzin",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Nie udało się zalogować, upewnij się, że dwuetapowe uwierzytelnianie (Autentykator lub SMS) jest aktywne.",
|
||||
"Invalid TFA code": "Niepoprawny kod TFA",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Nie udało się zalogować. To może być spowodowane wyłączoną dwustopniową autoryzacją na twoim koncie.",
|
||||
"Invalid answer": "Niepoprawna odpowiedź",
|
||||
"Invalid CAPTCHA": "CAPTCHA wykonane błędnie",
|
||||
"CAPTCHA is a required field": "CAPTCHA jest polem wymaganym",
|
||||
"User ID is a required field": "ID użytkownika jest polem wymaganym",
|
||||
"Password is a required field": "Hasło jest polem wymaganym",
|
||||
"Invalid username or password": "Niepoprawny login lub hasło",
|
||||
"Please sign in using 'Sign in with Google'": "Zaloguj się używając \"Zaloguj się przez Google\"",
|
||||
"Password cannot be empty": "Hasło nie może być puste",
|
||||
"Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków",
|
||||
"Please sign in": "Proszę się zalogować",
|
||||
"Invidious Private Feed for `x`": "",
|
||||
"channel:`x`": "kanał:`x",
|
||||
"Deleted or invalid channel": "Usunięty lub niepoprawny kanał",
|
||||
"This channel does not exist.": "Ten kanał nie istnieje.",
|
||||
"Could not get channel info.": "Nie udało się uzyskać informacji o kanale.",
|
||||
"Could not fetch comments": "Nie udało się pobrać komentarzy",
|
||||
"View `x` replies": "Wyświetl `x` odpowiedzi",
|
||||
"`x` ago": "`x` temu",
|
||||
"Load more": "Wczytaj więcej",
|
||||
"`x` points": "`x` punktów",
|
||||
"Could not create mix.": "Nie udało się utworzyć miksu.",
|
||||
"Playlist is empty": "Lista odtwarzania jest pusta",
|
||||
"Invalid playlist.": "Niepoprawna lista.",
|
||||
"Playlist does not exist.": "Lista odtwarzania nie istnieje.",
|
||||
"Could not pull trending pages.": "Nie udało się pobrać strony na czasie.",
|
||||
"Hidden field \"challenge\" is a required field": "Ukryte pole \"wyzwanie\" jest polem wymaganym",
|
||||
"Hidden field \"token\" is a required field": "Ukryte pole \"token\" jest polem wymaganym",
|
||||
"Invalid challenge": "Niepoprawne wyzwanie",
|
||||
"Invalid token": "Niepoprawny token",
|
||||
"Invalid user": "Niepoprawny użytkownik",
|
||||
"Token is expired, please try again": "Token wygasł, spróbuj ponownie",
|
||||
"English": "angielski",
|
||||
"English (auto-generated)": "angielski (automatycznie generowane)",
|
||||
"Afrikaans": "afrykanerski",
|
||||
"Albanian": "albański",
|
||||
"Amharic": "amharski",
|
||||
"Arabic": "arabski",
|
||||
"Armenian": "armeński",
|
||||
"Azerbaijani": "azerski",
|
||||
"Bangla": "bengalski",
|
||||
"Basque": "baskijski",
|
||||
"Belarusian": "białoruski",
|
||||
"Bosnian": "bośniacki",
|
||||
"Bulgarian": "bułgarski",
|
||||
"Burmese": "birmański",
|
||||
"Catalan": "kataloński",
|
||||
"Cebuano": "cebuański",
|
||||
"Chinese (Simplified)": "chiński (uproszczony)",
|
||||
"Chinese (Traditional)": "chiński (tradycyjny)",
|
||||
"Corsican": "korsykański",
|
||||
"Croatian": "chorwacki",
|
||||
"Czech": "czeski",
|
||||
"Danish": "duński",
|
||||
"Dutch": "holenderski",
|
||||
"Esperanto": "esperanto",
|
||||
"Estonian": "estoński",
|
||||
"Filipino": "filipiński",
|
||||
"Finnish": "fiński",
|
||||
"French": "francuski",
|
||||
"Galician": "galicyjski",
|
||||
"Georgian": "gruziński",
|
||||
"German": "niemiecki",
|
||||
"Greek": "grecki",
|
||||
"Gujarati": "gudźarati",
|
||||
"Haitian Creole": "kreolski haitański",
|
||||
"Hausa": "hausa",
|
||||
"Hawaiian": "hawajski",
|
||||
"Hebrew": "hebrajski",
|
||||
"Hindi": "hindi",
|
||||
"Hmong": "hmong",
|
||||
"Hungarian": "węgierski",
|
||||
"Icelandic": "islandzki",
|
||||
"Igbo": "ibo",
|
||||
"Indonesian": "indonezyjski",
|
||||
"Irish": "irlandzki",
|
||||
"Italian": "włoski",
|
||||
"Japanese": "japoński",
|
||||
"Javanese": "jawajski",
|
||||
"Kannada": "kannada",
|
||||
"Kazakh": "kazachski",
|
||||
"Khmer": "khmerski",
|
||||
"Korean": "koreański",
|
||||
"Kurdish": "kurdyjski",
|
||||
"Kyrgyz": "kirgiski",
|
||||
"Lao": "laotański",
|
||||
"Latin": "łaciński",
|
||||
"Latvian": "łotewski",
|
||||
"Lithuanian": "litewski",
|
||||
"Luxembourgish": "luksemburski",
|
||||
"Macedonian": "macedoński",
|
||||
"Malagasy": "malgaski",
|
||||
"Malay": "malajski",
|
||||
"Malayalam": "malajalam",
|
||||
"Maltese": "maltański",
|
||||
"Maori": "maoryski",
|
||||
"Marathi": "marathi",
|
||||
"Mongolian": "mongolski",
|
||||
"Nepali": "nepalski",
|
||||
"Norwegian": "norweski",
|
||||
"Nyanja": "njandża",
|
||||
"Pashto": "paszto",
|
||||
"Persian": "perski",
|
||||
"Polish": "polski",
|
||||
"Portuguese": "portugalski",
|
||||
"Punjabi": "pendżabski",
|
||||
"Romanian": "rumuński",
|
||||
"Russian": "rosyjski",
|
||||
"Samoan": "samoański",
|
||||
"Scottish Gaelic": "gaelicki szkocki",
|
||||
"Serbian": "serbski",
|
||||
"Shona": "shona",
|
||||
"Sindhi": "sindhi",
|
||||
"Sinhala": "syngaleski",
|
||||
"Slovak": "słowacki",
|
||||
"Slovenian": "słoweński",
|
||||
"Somali": "somalijski",
|
||||
"Southern Sotho": "sotho południowy",
|
||||
"Spanish": "hiszpański",
|
||||
"Spanish (Latin America)": "hiszpański (ameryka łacińska)",
|
||||
"Sundanese": "sundajski",
|
||||
"Swahili": "suahili",
|
||||
"Swedish": "szwedzki",
|
||||
"Tajik": "tadżycki",
|
||||
"Tamil": "tamilski",
|
||||
"Telugu": "telugu",
|
||||
"Thai": "tajski",
|
||||
"Turkish": "turecki",
|
||||
"Ukrainian": "ukraiński",
|
||||
"Urdu": "urdu",
|
||||
"Uzbek": "uzbecki",
|
||||
"Vietnamese": "wietnamski",
|
||||
"Welsh": "walijski",
|
||||
"Western Frisian": "zachodniofryzyjski",
|
||||
"Xhosa": "xhosa",
|
||||
"Yiddish": "jidysz",
|
||||
"Yoruba": "joruba",
|
||||
"Zulu": "zuluski",
|
||||
"`x` years": "`x` lat",
|
||||
"`x` months": "`x` miesięcy",
|
||||
"`x` weeks": "`x` tygodni",
|
||||
"`x` days": "`x` dni",
|
||||
"`x` hours": "`x` godzin",
|
||||
"`x` minutes": "`x` minut",
|
||||
"`x` seconds": "`x` sekund",
|
||||
"Fallback comments: ": "Zastępcze komentarze: ",
|
||||
"Popular": "Popularne",
|
||||
"Top": "Najczęściej oglądane",
|
||||
"About": "Informacje",
|
||||
"Rating: ": "Ocena: ",
|
||||
"Language: ": "Język: ",
|
||||
"Default": "Domyślnie",
|
||||
"Music": "Muzyka",
|
||||
"Gaming": "Gry",
|
||||
"News": "Wiadomości",
|
||||
"Movies": "Filmy",
|
||||
"Download": "Pobierz",
|
||||
"Download as: ": "Pobierz jako: ",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "(edytowany)",
|
||||
"Youtube permalink of the comment": "Odnośnik bezpośredni do komentarza na YouTube",
|
||||
"`x` marked it with a ❤": "'x' oznaczonych ❤",
|
||||
"Audio mode": "Tryb audio",
|
||||
"Video mode": "Tryb wideo",
|
||||
"Videos": "Filmy",
|
||||
"Playlists": "Playlisty",
|
||||
"Current version: ": "Aktualna wersja: "
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ name: invidious
|
||||
version: 0.15.0
|
||||
|
||||
authors:
|
||||
- Omar Roth <omarroth@hotmail.com>
|
||||
- Omar Roth <omarroth@protonmail.com>
|
||||
|
||||
targets:
|
||||
invidious:
|
||||
|
510
src/invidious.cr
510
src/invidious.cr
File diff suppressed because it is too large
Load Diff
@ -10,13 +10,15 @@ end
|
||||
|
||||
class ChannelVideo
|
||||
add_mapping({
|
||||
id: String,
|
||||
title: String,
|
||||
published: Time,
|
||||
updated: Time,
|
||||
ucid: String,
|
||||
author: String,
|
||||
length_seconds: {type: Int32, default: 0},
|
||||
id: String,
|
||||
title: String,
|
||||
published: Time,
|
||||
updated: Time,
|
||||
ucid: String,
|
||||
author: String,
|
||||
length_seconds: {type: Int32, default: 0},
|
||||
live_now: {type: Bool, default: false},
|
||||
premiere_timestamp: {type: Time?, default: nil},
|
||||
})
|
||||
end
|
||||
|
||||
@ -117,10 +119,27 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
|
||||
author = entry.xpath_node("author/name").not_nil!.content
|
||||
ucid = entry.xpath_node("channelid").not_nil!.content
|
||||
|
||||
length_seconds = videos.select { |video| video.id == video_id }[0]?.try &.length_seconds
|
||||
channel_video = videos.select { |video| video.id == video_id }[0]?
|
||||
|
||||
length_seconds = channel_video.try &.length_seconds
|
||||
length_seconds ||= 0
|
||||
|
||||
video = ChannelVideo.new(video_id, title, published, Time.now, ucid, author, length_seconds)
|
||||
live_now = channel_video.try &.live_now
|
||||
live_now ||= false
|
||||
|
||||
premiere_timestamp = channel_video.try &.premiere_timestamp
|
||||
|
||||
video = ChannelVideo.new(
|
||||
video_id,
|
||||
title,
|
||||
published,
|
||||
Time.now,
|
||||
ucid,
|
||||
author,
|
||||
length_seconds,
|
||||
live_now,
|
||||
premiere_timestamp
|
||||
)
|
||||
|
||||
db.exec("UPDATE users SET notifications = notifications || $1 \
|
||||
WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, ucid)
|
||||
@ -128,9 +147,12 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
|
||||
video_array = video.to_a
|
||||
args = arg_array(video_array)
|
||||
|
||||
# We don't include the 'premire_timestamp' here because channel pages don't include them,
|
||||
# meaning the above timestamp is always null
|
||||
db.exec("INSERT INTO channel_videos VALUES (#{args}) \
|
||||
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
|
||||
updated = $4, ucid = $5, author = $6, length_seconds = $7", video_array)
|
||||
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
|
||||
live_now = $8", video_array)
|
||||
end
|
||||
else
|
||||
page = 1
|
||||
@ -157,7 +179,17 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
|
||||
end
|
||||
|
||||
count = nodeset.size
|
||||
videos = videos.map { |video| ChannelVideo.new(video.id, video.title, video.published, Time.now, video.ucid, video.author, video.length_seconds) }
|
||||
videos = videos.map { |video| ChannelVideo.new(
|
||||
video.id,
|
||||
video.title,
|
||||
video.published,
|
||||
Time.now,
|
||||
video.ucid,
|
||||
video.author,
|
||||
video.length_seconds,
|
||||
video.live_now,
|
||||
video.premiere_timestamp
|
||||
) }
|
||||
|
||||
videos.each do |video|
|
||||
ids << video.id
|
||||
@ -170,8 +202,12 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
|
||||
video_array = video.to_a
|
||||
args = arg_array(video_array)
|
||||
|
||||
db.exec("INSERT INTO channel_videos VALUES (#{args}) ON CONFLICT (id) DO UPDATE SET title = $2, \
|
||||
published = $3, updated = $4, ucid = $5, author = $6, length_seconds = $7", video_array)
|
||||
# We don't include the 'premire_timestamp' here because channel pages don't include them,
|
||||
# meaning the above timestamp is always null
|
||||
db.exec("INSERT INTO channel_videos VALUES (#{args}) \
|
||||
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
|
||||
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
|
||||
live_now = $8", video_array)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -308,13 +308,13 @@ def template_youtube_comments(comments, locale)
|
||||
<p>
|
||||
<b>
|
||||
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{child["author"]}</a>
|
||||
</b>
|
||||
</b>
|
||||
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||
<span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
|
||||
|
|
||||
<a href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "Youtube permalink of the comment")}">[YT]</a>
|
||||
|
|
||||
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
|
||||
|
|
||||
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
|
||||
END_HTML
|
||||
|
||||
if child["creatorHeart"]?
|
||||
@ -372,8 +372,8 @@ def template_reddit_comments(root, locale)
|
||||
|
||||
content = <<-END_HTML
|
||||
<p>
|
||||
<a href="javascript:void(0)" onclick="toggle_parent(this)">[ - ]</a>
|
||||
<b><a href="https://www.reddit.com/user/#{author}">#{author}</a></b>
|
||||
<a href="javascript:void(0)" onclick="toggle_parent(this)">[ - ]</a>
|
||||
<b><a href="https://www.reddit.com/user/#{author}">#{author}</a></b>
|
||||
#{translate(locale, "`x` points", number_with_separator(score))}
|
||||
#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}
|
||||
</p>
|
||||
|
133
src/invidious/helpers/handlers.cr
Normal file
133
src/invidious/helpers/handlers.cr
Normal file
@ -0,0 +1,133 @@
|
||||
module HTTP::Handler
|
||||
@@exclude_routes_tree = Radix::Tree(String).new
|
||||
|
||||
macro exclude(paths, method = "GET")
|
||||
class_name = {{@type.name}}
|
||||
method_downcase = {{method.downcase}}
|
||||
class_name_method = "#{class_name}/#{method_downcase}"
|
||||
({{paths}}).each do |path|
|
||||
@@exclude_routes_tree.add class_name_method + path, '/' + method_downcase + path
|
||||
end
|
||||
end
|
||||
|
||||
def exclude_match?(env : HTTP::Server::Context)
|
||||
@@exclude_routes_tree.find(radix_path(env.request.method, env.request.path)).found?
|
||||
end
|
||||
|
||||
private def radix_path(method : String, path : String)
|
||||
"#{self.class}/#{method.downcase}#{path}"
|
||||
end
|
||||
end
|
||||
|
||||
class Kemal::RouteHandler
|
||||
exclude ["/api/v1/*"]
|
||||
|
||||
# Processes the route if it's a match. Otherwise renders 404.
|
||||
private def process_request(context)
|
||||
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
|
||||
content = context.route.handler.call(context)
|
||||
|
||||
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)
|
||||
raise Kemal::Exceptions::CustomException.new(context)
|
||||
end
|
||||
|
||||
context.response.print(content)
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
class Kemal::ExceptionHandler
|
||||
exclude ["/api/v1/*"]
|
||||
|
||||
private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32)
|
||||
return if context.response.closed?
|
||||
return if exclude_match? context
|
||||
|
||||
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(status_code)
|
||||
context.response.content_type = "text/html" unless context.response.headers.has_key?("Content-Type")
|
||||
context.response.status_code = status_code
|
||||
context.response.print Kemal.config.error_handlers[status_code].call(context, exception)
|
||||
context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class FilteredCompressHandler < Kemal::Handler
|
||||
exclude ["/videoplayback", "/videoplayback/*", "/vi/*", "/ggpht/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env if exclude_match? env
|
||||
|
||||
{% if flag?(:without_zlib) %}
|
||||
call_next env
|
||||
{% else %}
|
||||
request_headers = env.request.headers
|
||||
|
||||
if request_headers.includes_word?("Accept-Encoding", "gzip")
|
||||
env.response.headers["Content-Encoding"] = "gzip"
|
||||
env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
|
||||
elsif request_headers.includes_word?("Accept-Encoding", "deflate")
|
||||
env.response.headers["Content-Encoding"] = "deflate"
|
||||
env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
|
||||
end
|
||||
|
||||
call_next env
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
|
||||
class APIHandler < Kemal::Handler
|
||||
only ["/api/v1/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env unless only_match? env
|
||||
|
||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
# Here we swap out the socket IO so we can modify the response as needed
|
||||
output = env.response.output
|
||||
env.response.output = IO::Memory.new
|
||||
|
||||
begin
|
||||
call_next env
|
||||
|
||||
env.response.output.rewind
|
||||
response = env.response.output.gets_to_end
|
||||
|
||||
if env.response.headers["Content-Type"]?.try &.== "application/json"
|
||||
response = JSON.parse(response)
|
||||
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
response = response.to_pretty_json
|
||||
else
|
||||
response = response.to_json
|
||||
end
|
||||
end
|
||||
rescue
|
||||
ensure
|
||||
env.response.output = output
|
||||
env.response.puts response
|
||||
|
||||
env.response.flush
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class DenyFrame < Kemal::Handler
|
||||
exclude ["/embed/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env if exclude_match? env
|
||||
|
||||
env.response.headers["X-Frame-Options"] = "sameorigin"
|
||||
call_next env
|
||||
end
|
||||
end
|
||||
|
||||
# Temp fix for https://github.com/crystal-lang/crystal/issues/7383
|
||||
class HTTP::Client
|
||||
private def handle_response(response)
|
||||
# close unless response.keep_alive?
|
||||
response
|
||||
end
|
||||
end
|
@ -1,7 +1,5 @@
|
||||
class Config
|
||||
YAML.mapping({
|
||||
video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
|
||||
crawl_threads: Int32, # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
|
||||
channel_threads: Int32, # Number of threads to use for crawling videos from channels (for updating subscriptions)
|
||||
feed_threads: Int32, # Number of threads to use for updating feeds
|
||||
db: NamedTuple( # Database configuration
|
||||
@ -28,61 +26,6 @@ user: String,
|
||||
})
|
||||
end
|
||||
|
||||
class FilteredCompressHandler < Kemal::Handler
|
||||
exclude ["/videoplayback", "/videoplayback/*", "/vi/*", "/api/*", "/ggpht/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env if exclude_match? env
|
||||
|
||||
{% if flag?(:without_zlib) %}
|
||||
call_next env
|
||||
{% else %}
|
||||
request_headers = env.request.headers
|
||||
|
||||
if request_headers.includes_word?("Accept-Encoding", "gzip")
|
||||
env.response.headers["Content-Encoding"] = "gzip"
|
||||
env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
|
||||
elsif request_headers.includes_word?("Accept-Encoding", "deflate")
|
||||
env.response.headers["Content-Encoding"] = "deflate"
|
||||
env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
|
||||
end
|
||||
|
||||
call_next env
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
|
||||
class APIHandler < Kemal::Handler
|
||||
only ["/api/v1/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env unless only_match? env
|
||||
|
||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
call_next env
|
||||
end
|
||||
end
|
||||
|
||||
class DenyFrame < Kemal::Handler
|
||||
exclude ["/embed/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env if exclude_match? env
|
||||
|
||||
env.response.headers["X-Frame-Options"] = "sameorigin"
|
||||
call_next env
|
||||
end
|
||||
end
|
||||
|
||||
# Temp fix for https://github.com/crystal-lang/crystal/issues/7383
|
||||
class HTTP::Client
|
||||
private def handle_response(response)
|
||||
# close unless response.keep_alive?
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
def rank_videos(db, n)
|
||||
top = [] of {Float64, String}
|
||||
|
||||
@ -325,6 +268,11 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||
paid = true
|
||||
end
|
||||
|
||||
premiere_timestamp = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/span[@class="localized-date"])).try &.["data-timestamp"]?.try &.to_i64
|
||||
if premiere_timestamp
|
||||
premiere_timestamp = Time.unix(premiere_timestamp)
|
||||
end
|
||||
|
||||
items << SearchVideo.new(
|
||||
title: title,
|
||||
id: id,
|
||||
@ -337,7 +285,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||
length_seconds: length_seconds,
|
||||
live_now: live_now,
|
||||
paid: paid,
|
||||
premium: premium
|
||||
premium: premium,
|
||||
premiere_timestamp: premiere_timestamp
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -1,51 +1,3 @@
|
||||
def crawl_videos(db, logger)
|
||||
ids = Deque(String).new
|
||||
random = Random.new
|
||||
|
||||
search(random.base64(3)).as(Tuple)[1].each do |video|
|
||||
if video.is_a?(SearchVideo)
|
||||
ids << video.id
|
||||
end
|
||||
end
|
||||
|
||||
loop do
|
||||
if ids.empty?
|
||||
search(random.base64(3)).as(Tuple)[1].each do |video|
|
||||
if video.is_a?(SearchVideo)
|
||||
ids << video.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
id = ids[0]
|
||||
video = get_video(id, db)
|
||||
rescue ex
|
||||
logger.write("#{id} : #{ex.message}\n")
|
||||
next
|
||||
ensure
|
||||
ids.delete(id)
|
||||
end
|
||||
|
||||
rvs = [] of Hash(String, String)
|
||||
video.info["rvs"]?.try &.split(",").each do |rv|
|
||||
rvs << HTTP::Params.parse(rv).to_h
|
||||
end
|
||||
|
||||
rvs.each do |rv|
|
||||
if rv.has_key?("id") && !db.query_one?("SELECT EXISTS (SELECT true FROM videos WHERE id = $1)", rv["id"], as: Bool)
|
||||
ids.delete(id)
|
||||
ids << rv["id"]
|
||||
if ids.size == 150
|
||||
ids.shift
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
|
||||
max_channel = Channel(Int32).new
|
||||
|
||||
@ -82,30 +34,14 @@ def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sleep 1.minute
|
||||
end
|
||||
end
|
||||
|
||||
max_channel.send(max_threads)
|
||||
end
|
||||
|
||||
def refresh_videos(db, logger)
|
||||
loop do
|
||||
db.query("SELECT id FROM videos ORDER BY updated") do |rs|
|
||||
rs.each do
|
||||
begin
|
||||
id = rs.read(String)
|
||||
video = get_video(id, db)
|
||||
rescue ex
|
||||
logger.write("#{id} : #{ex.message}\n")
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_feeds(db, logger, max_threads = 1)
|
||||
max_channel = Channel(Int32).new
|
||||
|
||||
@ -129,15 +65,26 @@ def refresh_feeds(db, logger, max_threads = 1)
|
||||
active_threads += 1
|
||||
spawn do
|
||||
begin
|
||||
db.query("SELECT * FROM #{view_name} LIMIT 1") do |rs|
|
||||
# View doesn't contain same number of rows as ChannelVideo
|
||||
if ChannelVideo.from_rs(rs)[0]?.try &.to_a.size.try &.!= rs.column_count
|
||||
db.exec("DROP MATERIALIZED VIEW #{view_name}")
|
||||
raise "valid schema does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
|
||||
rescue ex
|
||||
# Create view if it doesn't exist
|
||||
if ex.message.try &.ends_with? "does not exist"
|
||||
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
||||
SELECT * FROM channel_videos WHERE \
|
||||
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
|
||||
ORDER BY published DESC;")
|
||||
logger.write("CREATE #{view_name}")
|
||||
if ex.message.try &.ends_with?("does not exist")
|
||||
# While iterating through, we may have an email stored from a deleted account
|
||||
if db.query_one?("SELECT true FROM users WHERE email = $1", email, as: Bool)
|
||||
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
||||
SELECT * FROM channel_videos WHERE \
|
||||
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
|
||||
ORDER BY published DESC;")
|
||||
logger.write("CREATE #{view_name}\n")
|
||||
end
|
||||
else
|
||||
logger.write("REFRESH #{email} : #{ex.message}\n")
|
||||
end
|
||||
@ -147,6 +94,8 @@ def refresh_feeds(db, logger, max_threads = 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sleep 1.minute
|
||||
end
|
||||
end
|
||||
|
||||
@ -169,7 +118,6 @@ def subscribe_to_feeds(db, logger, key, config)
|
||||
end
|
||||
|
||||
sleep 1.minute
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -200,7 +148,7 @@ def pull_top_videos(config, db)
|
||||
end
|
||||
|
||||
yield videos
|
||||
Fiber.yield
|
||||
sleep 1.minute
|
||||
end
|
||||
end
|
||||
|
||||
@ -215,7 +163,7 @@ def pull_popular_videos(db)
|
||||
ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse
|
||||
|
||||
yield videos
|
||||
Fiber.yield
|
||||
sleep 1.minute
|
||||
end
|
||||
end
|
||||
|
||||
@ -228,6 +176,7 @@ def update_decrypt_function
|
||||
end
|
||||
|
||||
yield decrypt_function
|
||||
sleep 1.minute
|
||||
end
|
||||
end
|
||||
|
||||
@ -239,7 +188,8 @@ def find_working_proxies(regions)
|
||||
# proxies = filter_proxies(proxies)
|
||||
|
||||
yield region, proxies
|
||||
Fiber.yield
|
||||
end
|
||||
|
||||
sleep 1.minute
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ class PlaylistVideo
|
||||
published: Time,
|
||||
playlists: Array(String),
|
||||
index: Int32,
|
||||
live_now: Bool,
|
||||
})
|
||||
end
|
||||
|
||||
@ -101,8 +102,10 @@ def extract_playlist(plid, nodeset, index)
|
||||
anchor = video.xpath_node(%q(.//td[@class="pl-video-time"]/div/div[1]))
|
||||
if anchor && !anchor.content.empty?
|
||||
length_seconds = decode_length_seconds(anchor.content)
|
||||
live_now = false
|
||||
else
|
||||
length_seconds = 0
|
||||
live_now = true
|
||||
end
|
||||
|
||||
videos << PlaylistVideo.new(
|
||||
@ -114,6 +117,7 @@ def extract_playlist(plid, nodeset, index)
|
||||
published: Time.now,
|
||||
playlists: [plid],
|
||||
index: index + offset,
|
||||
live_now: live_now
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -1,17 +1,18 @@
|
||||
class SearchVideo
|
||||
add_mapping({
|
||||
title: String,
|
||||
id: String,
|
||||
author: String,
|
||||
ucid: String,
|
||||
published: Time,
|
||||
views: Int64,
|
||||
description: String,
|
||||
description_html: String,
|
||||
length_seconds: Int32,
|
||||
live_now: Bool,
|
||||
paid: Bool,
|
||||
premium: Bool,
|
||||
title: String,
|
||||
id: String,
|
||||
author: String,
|
||||
ucid: String,
|
||||
published: Time,
|
||||
views: Int64,
|
||||
description: String,
|
||||
description_html: String,
|
||||
length_seconds: Int32,
|
||||
live_now: Bool,
|
||||
paid: Bool,
|
||||
premium: Bool,
|
||||
premiere_timestamp: Time?,
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -255,8 +255,12 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
|
||||
challenge = OpenSSL::HMAC.digest(:sha256, key, challenge)
|
||||
challenge = Base64.urlsafe_encode(challenge)
|
||||
|
||||
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)
|
||||
db.exec("DELETE FROM nonces * WHERE nonce = $1", nonce)
|
||||
if nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", nonce, as: {String, Time})
|
||||
if nonce[1] > Time.now
|
||||
db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.new(1990, 1, 1), nonce[0])
|
||||
else
|
||||
raise translate(locale, "Invalid token")
|
||||
end
|
||||
else
|
||||
raise translate(locale, "Invalid token")
|
||||
end
|
||||
@ -270,7 +274,7 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
|
||||
end
|
||||
|
||||
if challenge_user_id != user_id
|
||||
raise translate(locale, "Invalid user")
|
||||
raise translate(locale, "Invalid token")
|
||||
end
|
||||
|
||||
if expire < Time.now.to_unix
|
||||
@ -296,7 +300,7 @@ def generate_captcha(key, db)
|
||||
clock_svg = <<-END_SVG
|
||||
<svg viewBox="0 0 100 100" width="200px">
|
||||
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
|
||||
|
||||
|
||||
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
|
||||
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
|
||||
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
|
||||
@ -328,7 +332,22 @@ def generate_captcha(key, db)
|
||||
answer = "#{hour}:#{minute.to_s.rjust(2, '0')}:#{second.to_s.rjust(2, '0')}"
|
||||
answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer)
|
||||
|
||||
challenge, token = create_response(answer, "sign_in", key, db)
|
||||
|
||||
return {image: image, challenge: challenge, token: token}
|
||||
return {
|
||||
question: image,
|
||||
tokens: [create_response(answer, "sign_in", key, db)],
|
||||
}
|
||||
end
|
||||
|
||||
def generate_text_captcha(key, db)
|
||||
response = HTTP::Client.get(TEXTCAPTCHA_URL).body
|
||||
response = JSON.parse(response)
|
||||
|
||||
tokens = response["a"].as_a.map do |answer|
|
||||
create_response(answer.as_s, "sign_in", key, db)
|
||||
end
|
||||
|
||||
return {
|
||||
question: response["q"].as_s,
|
||||
tokens: tokens,
|
||||
}
|
||||
end
|
||||
|
@ -250,6 +250,63 @@ class Video
|
||||
end
|
||||
end
|
||||
|
||||
def allow_ratings
|
||||
allow_ratings = player_response["videoDetails"].try &.["allowRatings"]?.try &.as_bool
|
||||
|
||||
if allow_ratings.nil?
|
||||
return true
|
||||
end
|
||||
|
||||
return allow_ratings
|
||||
end
|
||||
|
||||
def live_now
|
||||
live_now = self.player_response["videoDetails"]?.try &.["isLive"]?.try &.as_bool
|
||||
|
||||
if live_now.nil?
|
||||
return false
|
||||
end
|
||||
|
||||
return live_now
|
||||
end
|
||||
|
||||
def is_listed
|
||||
is_listed = player_response["videoDetails"].try &.["isCrawlable"]?.try &.as_bool
|
||||
|
||||
if is_listed.nil?
|
||||
return true
|
||||
end
|
||||
|
||||
return is_listed
|
||||
end
|
||||
|
||||
def is_upcoming
|
||||
is_upcoming = player_response["videoDetails"].try &.["isUpcoming"]?.try &.as_bool
|
||||
|
||||
if is_upcoming.nil?
|
||||
return false
|
||||
end
|
||||
|
||||
return is_upcoming
|
||||
end
|
||||
|
||||
def premiere_timestamp
|
||||
if self.is_upcoming
|
||||
premiere_timestamp = player_response["playabilityStatus"]?
|
||||
.try &.["liveStreamability"]?
|
||||
.try &.["liveStreamabilityRenderer"]?
|
||||
.try &.["offlineSlate"]?
|
||||
.try &.["liveStreamOfflineSlateRenderer"]?
|
||||
.try &.["scheduledStartTime"].as_s.to_i64
|
||||
end
|
||||
|
||||
if premiere_timestamp
|
||||
premiere_timestamp = Time.unix(premiere_timestamp)
|
||||
end
|
||||
|
||||
return premiere_timestamp
|
||||
end
|
||||
|
||||
def keywords
|
||||
keywords = self.player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a
|
||||
keywords ||= [] of String
|
||||
@ -644,6 +701,10 @@ def fetch_video(id, proxies, region)
|
||||
raise "Video unavailable."
|
||||
end
|
||||
|
||||
if !info["title"]?
|
||||
raise "Video unavailable."
|
||||
end
|
||||
|
||||
title = info["title"]
|
||||
author = info["author"]
|
||||
ucid = info["ucid"]
|
||||
|
@ -64,7 +64,7 @@
|
||||
<%= rendered "components/item" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
|
@ -39,7 +39,9 @@
|
||||
<% else %>
|
||||
<div class="thumbnail">
|
||||
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
||||
<% if item.length_seconds != 0 %>
|
||||
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<p><%= item.title %></p>
|
||||
@ -55,7 +57,7 @@
|
||||
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
||||
<% if item.responds_to?(:live_now) && item.live_now %>
|
||||
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
|
||||
<% else %>
|
||||
<% elsif item.length_seconds != 0 %>
|
||||
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
@ -65,8 +67,10 @@
|
||||
<p>
|
||||
<b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
|
||||
</p>
|
||||
|
||||
<% if Time.now - item.published > 1.minute %>
|
||||
|
||||
<% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp && item.premiere_timestamp.not_nil! > Time.now %>
|
||||
<h5><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.now).ago, locale)) %></h5>
|
||||
<% elsif Time.now - item.published > 1.minute %>
|
||||
<h5><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></h5>
|
||||
<% end %>
|
||||
<% else %>
|
||||
@ -90,7 +94,7 @@
|
||||
<% end %>
|
||||
<% if item.responds_to?(:live_now) && item.live_now %>
|
||||
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
|
||||
<% else %>
|
||||
<% elsif item.length_seconds != 0 %>
|
||||
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
@ -100,8 +104,10 @@
|
||||
<p>
|
||||
<b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
|
||||
</p>
|
||||
|
||||
<% if Time.now - item.published > 1.minute %>
|
||||
|
||||
<% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp && item.premiere_timestamp.not_nil! > Time.now %>
|
||||
<h5><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.now).ago, locale)) %></h5>
|
||||
<% elsif Time.now - item.published > 1.minute %>
|
||||
<h5><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></h5>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<video style="outline:none;width:100%" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
|
||||
<video style="outline:none;width:100%" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
|
||||
id="player" class="video-js"
|
||||
onmouseenter='this["data-title"]=this["title"];this["title"]=""'
|
||||
onmouseleave='this["title"]=this["data-title"];this["data-title"]=""'
|
||||
@ -44,7 +44,7 @@ var options = {
|
||||
aspectRatio: "<%= aspect_ratio %>",
|
||||
<% end %>
|
||||
preload: "auto",
|
||||
playbackRates: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
|
||||
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
|
||||
controlBar: {
|
||||
children: [
|
||||
"playToggle",
|
||||
@ -78,6 +78,7 @@ var player = videojs("player", options, function() {
|
||||
volumeStep: 0.1,
|
||||
seekStep: 5,
|
||||
enableModifiersForNumbers: false,
|
||||
enableHoverScroll: true,
|
||||
customKeys: {
|
||||
// Toggle play with K Key
|
||||
play: {
|
||||
@ -151,7 +152,7 @@ player.on('error', function(event) {
|
||||
}
|
||||
player.currentTime(currentTime);
|
||||
player.playbackRate(playbackRate);
|
||||
|
||||
|
||||
if (!paused) {
|
||||
player.play();
|
||||
}
|
||||
@ -191,7 +192,7 @@ var bpb = player.getChild('bigPlayButton');
|
||||
|
||||
if (bpb) {
|
||||
bpb.hide();
|
||||
|
||||
|
||||
player.ready(function() {
|
||||
new Promise(function(resolve, reject) {
|
||||
setTimeout(() => resolve(1), 1);
|
||||
|
@ -1,14 +1,14 @@
|
||||
<% if user %>
|
||||
<% if subscriptions.includes? ucid %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
|
||||
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
|
||||
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
|
||||
</a>
|
||||
@ -16,7 +16,7 @@
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" class="pure-button pure-button-primary"
|
||||
<a id="subscribe" class="pure-button pure-button-primary"
|
||||
href="/login?referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Login to subscribe to `x`", author) %></b>
|
||||
</a>
|
||||
|
@ -4,13 +4,13 @@ if (subscribe_button.getAttribute('onclick')) {
|
||||
}
|
||||
|
||||
function subscribe(timeouts = 0) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
|
||||
if (timeouts > 10) {
|
||||
console.log("Failed to subscribe.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var url = "/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
@ -21,7 +21,7 @@ function subscribe(timeouts = 0) {
|
||||
var fallback = subscribe_button.innerHTML;
|
||||
subscribe_button.onclick = unsubscribe;
|
||||
subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe").gsub("'", "\\'") %> | <%= sub_count_text %></b>'
|
||||
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status != 200) {
|
||||
@ -30,7 +30,7 @@ function subscribe(timeouts = 0) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
console.log("Subscribing timed out.");
|
||||
|
||||
@ -39,8 +39,8 @@ function subscribe(timeouts = 0) {
|
||||
}
|
||||
|
||||
function unsubscribe(timeouts = 0) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
|
||||
if (timeouts > 10) {
|
||||
console.log("Failed to subscribe");
|
||||
return;
|
||||
|
@ -20,7 +20,7 @@
|
||||
</label>
|
||||
<input type="file" id="import_youtube" name="import_youtube">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_freetube"><%= translate(locale, "Import FreeTube subscriptions (.db)") %></label>
|
||||
<input type="file" id="import_freetube" name="import_freetube">
|
||||
@ -35,7 +35,7 @@
|
||||
<label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label>
|
||||
<input type="file" id="import_newpipe" name="import_newpipe">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pure-controls">
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button>
|
||||
</div>
|
||||
|
@ -10,13 +10,13 @@
|
||||
<title><%= HTML.escape(video.title) %> - Invidious</title>
|
||||
<style>
|
||||
#player {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 100%;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: -100;
|
||||
}
|
||||
</style>
|
||||
|
@ -19,7 +19,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://unpkg.com/dashjs@2.9.0/dist/dash.mediaplayer.debug.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/Dash-Industry-Forum/dash.js"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="/js/silvermine-videojs-quality-selector.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/omarroth/videojs-quality-selector"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://unpkg.com/video.js@6.12.1/dist/video.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/videojs/video.js"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://unpkg.com/videojs-contrib-quality-levels@2.0.7/dist/videojs-contrib-quality-levels.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://unpkg.com/videojs-contrib-dash@2.8.2/dist/videojs-dash.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/videojs/videojs-contrib-dash"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -89,7 +89,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://unpkg.com/@videojs/http-streaming@1.2.2/dist/videojs-http-streaming.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/videojs/http-streaming"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -103,7 +103,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://unpkg.com/videojs-markers@1.0.1/dist/videojs-markers.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/spchuang/videojs-markers"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -117,7 +117,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://unpkg.com/videojs-share@2.0.1/dist/videojs-share.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/mkhazov/videojs-share"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -131,7 +131,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="/js/videojs.hotkeys.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/ctd1500/videojs-hotkeys"><%= translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div class="h-box">
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-2">
|
||||
<a class="pure-button <% if account_type == "invidious" %>pure-button-disabled<% end %>" href="/login">
|
||||
<a class="pure-button <% if account_type == "invidious" %>pure-button-disabled<% end %>" href="/login?type=invidious">
|
||||
<%= translate(locale, "Login/Register") %>
|
||||
</a>
|
||||
</div>
|
||||
@ -22,55 +22,84 @@
|
||||
<% if account_type == "invidious" %>
|
||||
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=invidious" method="post">
|
||||
<fieldset>
|
||||
<% if email %>
|
||||
<input name="email" type="hidden" value="<%= email %>">
|
||||
<% else %>
|
||||
<label for="email"><%= translate(locale, "User ID:") %></label>
|
||||
<input required class="pure-input-1" name="email" type="text" placeholder="User ID">
|
||||
<% end %>
|
||||
|
||||
<% if password %>
|
||||
<input name="password" type="hidden" value="<%= password %>">
|
||||
<% else %>
|
||||
<label for="password"><%= translate(locale, "Password:") %></label>
|
||||
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
|
||||
<% end %>
|
||||
|
||||
<% if config.captcha_enabled %>
|
||||
<% if captcha_type == "image" %>
|
||||
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
|
||||
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
||||
<input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
|
||||
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
|
||||
<input required type="text" name="answer" type="text" placeholder="h:mm:ss">
|
||||
|
||||
<label>
|
||||
<a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">
|
||||
<%= translate(locale, "Text CAPTCHA") %>
|
||||
</a>
|
||||
</label>
|
||||
<% else %>
|
||||
<% text_captcha.not_nil![:tokens].each_with_index do |token, i| %>
|
||||
<input type="hidden" name="text_challenge<%= i %>" value="<%= token[0] %>">
|
||||
<input type="hidden" name="text_token<%= i %>" value="<%= token[1] %>">
|
||||
<% if captcha %>
|
||||
<% case captcha_type when %>
|
||||
<% when "image" %>
|
||||
<% captcha = captcha.not_nil! %>
|
||||
<img style="width:100%" src='<%= captcha[:question] %>'/>
|
||||
<% captcha[:tokens].each_with_index do |token, i| %>
|
||||
<input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>">
|
||||
<input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>">
|
||||
<% end %>
|
||||
<label for="text_answer"><%= text_captcha.not_nil![:question] %></label>
|
||||
<input required type="text" name="text_answer" type="text" placeholder="Answer">
|
||||
<input type="hidden" name="captcha_type" value="image">
|
||||
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
|
||||
<input type="text" name="answer" type="text" placeholder="h:mm:ss">
|
||||
<% when "text" %>
|
||||
<% captcha = captcha.not_nil! %>
|
||||
<% captcha[:tokens].each_with_index do |token, i| %>
|
||||
<input type="hidden" name="challenge[<%= i %>]" value="<%= token[0] %>">
|
||||
<input type="hidden" name="token[<%= i %>]" value="<%= token[1] %>">
|
||||
<% end %>
|
||||
<input type="hidden" name="captcha_type" value="text">
|
||||
<label for="answer"><%= captcha[:question] %></label>
|
||||
<input type="text" name="answer" type="text" placeholder="Answer">
|
||||
<% end %>
|
||||
|
||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Register") %>
|
||||
</button>
|
||||
|
||||
<% case captcha_type when %>
|
||||
<% when "image" %>
|
||||
<label>
|
||||
<a href="/login?referer=<%= URI.escape(referer) %>&type=invidious">
|
||||
<button type="submit" name="change_type" class="pure-button pure-button-primary" value="text">
|
||||
<%= translate(locale, "Text CAPTCHA") %>
|
||||
</button>
|
||||
</label>
|
||||
<% when "text" %>
|
||||
<label>
|
||||
<button type="submit" name="change_type" class="pure-button pure-button-primary" value="image">
|
||||
<%= translate(locale, "Image CAPTCHA") %>
|
||||
</a>
|
||||
</button>
|
||||
</label>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button>
|
||||
<% if config.registration_enabled %>
|
||||
<button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
</form>
|
||||
<% elsif account_type == "google" %>
|
||||
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>" method="post">
|
||||
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=google" method="post">
|
||||
<fieldset>
|
||||
<% if email %>
|
||||
<input name="email" type="hidden" value="<%= email %>">
|
||||
<% else %>
|
||||
<label for="email"><%= translate(locale, "Email:") %></label>
|
||||
<input required class="pure-input-1" name="email" type="email" placeholder="Email">
|
||||
<% end %>
|
||||
|
||||
<% if password %>
|
||||
<input name="password" type="hidden" value="<%= password %>">
|
||||
<% else %>
|
||||
<label for="password"><%= translate(locale, "Password:") %></label>
|
||||
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
|
||||
<% end %>
|
||||
|
||||
<% if tfa %>
|
||||
<label for="tfa"><%= translate(locale, "Google verification code:") %></label>
|
||||
|
@ -19,4 +19,4 @@
|
||||
<%= rendered "components/item" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,7 +31,7 @@
|
||||
<%= rendered "components/item" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
|
@ -41,7 +41,7 @@ function update_value(element) {
|
||||
<div class="pure-control-group">
|
||||
<label for="speed"><%= translate(locale, "Default speed: ") %></label>
|
||||
<select name="speed" id="speed">
|
||||
<% {2.0, 1.5, 1.25, 1.0, 0.75, 0.5}.each do |option| %>
|
||||
<% {2.0, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
|
||||
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
|
@ -5,15 +5,15 @@
|
||||
<div class="h-box">
|
||||
<%= Markdown.to_html(<<-END_PRIVACY_POLICY
|
||||
## Privacy
|
||||
|
||||
|
||||
This document concerns what data you provide to this website, the purpose of the data, how the data is stored, and how the data can be removed.
|
||||
|
||||
|
||||
### Data you directly provide
|
||||
|
||||
|
||||
Data that you provide to the website for the purpose of the site's operation (for example: an account name, account password, or channel subscription) will be stored in the website's database until the user decides to remove it. This data will not be intentionally shared with anyone or anything.
|
||||
|
||||
|
||||
Information stored about a registered user is limited to:
|
||||
|
||||
|
||||
- a list of session tokens for remaining logged in across devices
|
||||
- the last time an account was updated (to provide accurate notifications)
|
||||
- a list of video IDs identifying notifications from a user's subscriptions
|
||||
@ -23,51 +23,51 @@
|
||||
- a hashed password if applicable (not present on google accounts)
|
||||
- a randomly generated token for providing an RSS feed of a user's subscriptions
|
||||
- a list of video IDs identifying watched videos
|
||||
|
||||
|
||||
The above list reflects [this code](https://github.com/omarroth/invidious/blob/master/src/invidious/users.cr#L14-L51).
|
||||
|
||||
|
||||
Users can clear their watch history using the [clear watch history](/clear_watch_history) page.
|
||||
|
||||
|
||||
If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.
|
||||
|
||||
|
||||
### Data you passively provide
|
||||
|
||||
|
||||
When you request any resource from this website (for example: a page, a font, an image, or an API endpoint) information about the request may be logged.
|
||||
|
||||
|
||||
Information about a request is limited to:
|
||||
|
||||
|
||||
- the time the request was made
|
||||
- the status code of the response
|
||||
- the method of the request
|
||||
- the requested URL
|
||||
- how long it took to complete the request.
|
||||
|
||||
|
||||
No identifying information is logged, such as the visitor's cookie, user-agent, or IP address. Here are a couple lines to serve as an example:
|
||||
|
||||
|
||||
```
|
||||
2019-01-19 16:37:47 +00:00 200 GET /api/v1/comments/xrlETJYzH-c?format=html&hl=en-US 1345.88ms
|
||||
2019-01-19 16:37:53 +00:00 200 GET /vi/r5P-f5arPXE/maxres.jpg 1085.41ms
|
||||
2019-01-19 16:37:54 +00:00 200 GET /watch 7.04ms
|
||||
```
|
||||
|
||||
|
||||
This website does not store the visitor's user-agent or IP address and does not use fingerprinting, advertisements, or tracking of any form.
|
||||
|
||||
|
||||
This website provides links to googlevideo.com to provide audio and video playback. googlevideo.com is owned by Google and is subject to their [privacy policy](https://policies.google.com/privacy).
|
||||
|
||||
|
||||
### Data stored in your browser
|
||||
|
||||
|
||||
This website uses browser cookies to authenticate registered users. This data consists of:
|
||||
|
||||
|
||||
- An account token to keep you logged into the website between visits, which is sent when any page is loaded while you are logged in
|
||||
|
||||
|
||||
This website also provides an option to store site preferences, such as the theme or locale, without an account. Using this feature will store a cookie in the visitor's browser containing their preferences. This cookie is sent on every request and does not contain any identifying information.
|
||||
|
||||
You can remove this data from your browser by logging out of this website, or by using your browser's cookie-related controls to delete the data.
|
||||
|
||||
|
||||
### Removal of data
|
||||
|
||||
|
||||
To remove data stored in your browser, you can log out of the website, or you can use your browser's cookie-related controls to delete the data.
|
||||
|
||||
|
||||
To remove data that has been stored in the website's database, you can use the [delete my account](/delete_account) page.
|
||||
END_PRIVACY_POLICY
|
||||
)
|
||||
|
@ -8,7 +8,7 @@
|
||||
<%= rendered "components/item" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
|
@ -181,50 +181,35 @@
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-bitcoin"></i>
|
||||
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-bitcoin"></i>
|
||||
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-usd"></i>
|
||||
<a href="https://liberapay.com/omarroth">Liberapay</a>
|
||||
/
|
||||
<a href="https://patreon.com/omarroth">Patreon</a>
|
||||
</div>
|
||||
<div class="content">
|
||||
<%= content %>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-javascript"></i>
|
||||
<a rel="jslicense" href="/licenses">
|
||||
<%= translate(locale, "View JavaScript license information.") %>
|
||||
</a>
|
||||
/
|
||||
<i class="icon ion-ios-paper"></i>
|
||||
<a href="/privacy">
|
||||
<%= translate(locale, "View privacy policy.") %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<a href="https://github.com/omarroth/invidious">
|
||||
<%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-bitcoin"></i>
|
||||
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-bitcoin"></i>
|
||||
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-usd"></i>
|
||||
<a href="https://liberapay.com/omarroth">Liberapay</a>
|
||||
/
|
||||
<a href="https://patreon.com/omarroth">Patreon</a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-javascript"></i>
|
||||
<a rel="jslicense" href="/licenses">
|
||||
<%= translate(locale, "View JavaScript license information.") %>
|
||||
</a>
|
||||
/
|
||||
<i class="icon ion-ios-paper"></i>
|
||||
<a href="/privacy">
|
||||
<%= translate(locale, "View privacy policy.") %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-github"></i>
|
||||
<%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %>
|
||||
<i class="icon ion-logo-github"></i>
|
||||
<%= CURRENT_BRANCH %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<i class="icon ion-logo-github"></i>
|
||||
<%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %>
|
||||
<i class="icon ion-logo-github"></i>
|
||||
<%= CURRENT_BRANCH %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/ui.js"></script>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<meta name="twitter:url" content="<%= host_url %>/watch?v=<%= video.id %>">
|
||||
<meta name="twitter:title" content="<%= HTML.escape(video.title) %>">
|
||||
<meta name="twitter:description" content="<%= description %>">
|
||||
<meta name="twitter:image" content="/vi/<%= video.id %>/hqdefault.jpg">
|
||||
<meta name="twitter:image" content="<%= host_url %>/vi/<%= video.id %>/maxres.jpg">
|
||||
<meta name="twitter:player" content="<%= host_url %>/embed/<%= video.id %>">
|
||||
<meta name="twitter:player:width" content="1280">
|
||||
<meta name="twitter:player:height" content="720">
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
<div class="h-box">
|
||||
<h1>
|
||||
<%= HTML.escape(video.title) %>
|
||||
<%= HTML.escape(video.title) %>
|
||||
<% if params[:listen] %>
|
||||
<a title="<%=translate(locale, "Video mode")%>" href="/watch?<%= env.params.query %>&listen=0">
|
||||
<i class="icon ion-ios-videocam"></i>
|
||||
@ -53,23 +53,23 @@
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
<div class="h-box">
|
||||
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p>
|
||||
|
||||
|
||||
<form class="pure-form pure-form-stacked" action="/latest_version" method="get" rel="noopener" target="_blank">
|
||||
<div class="pure-control-group">
|
||||
<label for="download_widget"><%= translate(locale, "Download as: ") %></label>
|
||||
<select style="width:100%" name="download_widget" id="download_widget">
|
||||
<% video_streams.each do |option| %>
|
||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split("/")[1].split(";")[0] %>"}'>
|
||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
|
||||
<%= option["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only
|
||||
</option>
|
||||
<% end %>
|
||||
<% audio_streams.each do |option| %>
|
||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split("/")[1].split(";")[0] %>"}'>
|
||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
|
||||
<%= option["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only
|
||||
</option>
|
||||
<% end %>
|
||||
<% fmt_stream.each do |option| %>
|
||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split("/")[1].split(";")[0] %>"}'>
|
||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
|
||||
<%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %>
|
||||
</option>
|
||||
<% end %>
|
||||
@ -150,7 +150,7 @@
|
||||
|
||||
<% if params[:related_videos] %>
|
||||
<div class="h-box">
|
||||
|
||||
|
||||
<% if !rvs.empty? %>
|
||||
<div id="continue" <% if plid %>style="display:none"<% end %>>
|
||||
<div class="pure-control-group">
|
||||
@ -187,7 +187,7 @@
|
||||
<script>
|
||||
<% if !rvs.empty? && !plid && params[:continue] %>
|
||||
player.on('ended', function() {
|
||||
location.assign("/watch?v="
|
||||
location.assign("/watch?v="
|
||||
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
|
||||
+ "&continue=1"
|
||||
<% if params[:listen] %>
|
||||
@ -206,7 +206,7 @@ player.on('ended', function() {
|
||||
function continue_autoplay(target) {
|
||||
if (target.checked) {
|
||||
player.on('ended', function() {
|
||||
location.assign("/watch?v="
|
||||
location.assign("/watch?v="
|
||||
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
|
||||
+ "&continue=1"
|
||||
<% if params[:listen] %>
|
||||
@ -249,7 +249,7 @@ function get_playlist(timeouts = 0) {
|
||||
}
|
||||
|
||||
playlist.innerHTML = ' \
|
||||
<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3> \
|
||||
<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3> \
|
||||
<hr>'
|
||||
|
||||
var plid = "<%= plid %>"
|
||||
@ -270,10 +270,10 @@ function get_playlist(timeouts = 0) {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
playlist.innerHTML = xhr.response.playlistHtml;
|
||||
|
||||
|
||||
if (xhr.response.nextVideo) {
|
||||
player.on('ended', function() {
|
||||
location.assign("/watch?v="
|
||||
location.assign("/watch?v="
|
||||
+ xhr.response.nextVideo
|
||||
+ "&list=<%= plid %>"
|
||||
<% if params[:listen] %>
|
||||
@ -300,7 +300,7 @@ function get_playlist(timeouts = 0) {
|
||||
|
||||
comments = document.getElementById("playlist");
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3><hr>';
|
||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3><hr>';
|
||||
get_playlist(timeouts + 1);
|
||||
};
|
||||
}
|
||||
@ -319,7 +319,7 @@ function get_reddit_comments(timeouts = 0) {
|
||||
|
||||
var fallback = comments.innerHTML;
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
||||
|
||||
var url = "/api/v1/comments/<%= video.id %>?source=reddit&format=html&hl=<%= env.get("preferences").as(Preferences).locale %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
@ -355,7 +355,7 @@ function get_reddit_comments(timeouts = 0) {
|
||||
contentHtml: xhr.response.contentHtml
|
||||
});
|
||||
} else {
|
||||
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||
get_youtube_comments();
|
||||
<% else %>
|
||||
comments.innerHTML = fallback;
|
||||
@ -382,7 +382,7 @@ function get_youtube_comments(timeouts = 0) {
|
||||
|
||||
var fallback = comments.innerHTML;
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
||||
|
||||
var url = "/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("preferences").as(Preferences).locale %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
@ -416,7 +416,7 @@ function get_youtube_comments(timeouts = 0) {
|
||||
comments.innerHTML = "";
|
||||
}
|
||||
} else {
|
||||
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||
get_youtube_comments();
|
||||
<% else %>
|
||||
comments.innerHTML = "";
|
||||
@ -429,7 +429,7 @@ function get_youtube_comments(timeouts = 0) {
|
||||
console.log("Pulling comments timed out.");
|
||||
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
||||
get_youtube_comments(timeouts + 1);
|
||||
};
|
||||
}
|
||||
@ -440,7 +440,7 @@ function get_youtube_replies(target, load_more) {
|
||||
var body = target.parentNode.parentNode;
|
||||
var fallback = body.innerHTML;
|
||||
body.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
||||
|
||||
var url = '/api/v1/comments/<%= video.id %>?format=html&hl=<%= env.get("preferences").as(Preferences).locale %>&continuation=' +
|
||||
continuation;
|
||||
|
Loading…
Reference in New Issue
Block a user