forked from midou/invidious
Compare commits
99 Commits
Author | SHA1 | Date | |
---|---|---|---|
35bee987f6 | |||
bd5ec2f2f3 | |||
296771809a | |||
83ba4e2a4c | |||
6cb834a18d | |||
0a4e9e6252 | |||
9619d3f1bc | |||
f39ed3d145 | |||
f38aac851e | |||
b6adeb80e6 | |||
c74cc1123f | |||
0e1b5d7cdd | |||
d2bbf9d33c | |||
3ccee120d3 | |||
6753294ee1 | |||
f9881ebaab | |||
429a4b2dec | |||
4287c0d96a | |||
5cd137d808 | |||
62ae836565 | |||
b7acdfad24 | |||
d3eadccd51 | |||
2232bc0495 | |||
f7ca81c384 | |||
d4ee786cab | |||
a54668688b | |||
89bda1d3db | |||
e0ee1c3d79 | |||
5b2c228bb6 | |||
ffab3ee79f | |||
dc6cc028c5 | |||
c1f17f2f82 | |||
1c8bd671d8 | |||
133b72f9cf | |||
8c45694ce5 | |||
bd820b9b48 | |||
47e94fedc6 | |||
aff2083529 | |||
1eae76fc15 | |||
cf63c825d4 | |||
446d8569a4 | |||
454b1662b7 | |||
3ec684ae71 | |||
b17d3d1e51 | |||
d81a803618 | |||
e6d2166bac | |||
e590d39aa9 | |||
4f91854bd3 | |||
29a21860ae | |||
96234e509f | |||
a749ac73ac | |||
62f023c50f | |||
29dc114f7a | |||
023066b452 | |||
93e12d94fc | |||
044a57ef34 | |||
bc49c7d181 | |||
5632e58636 | |||
e1bf7fa6cc | |||
40028e1462 | |||
53732cdcab | |||
2ac89d5e00 | |||
98d71ca8e7 | |||
0f2f273335 | |||
000cfd4834 | |||
25c3ee034e | |||
89d3587861 | |||
0d8f036bf1 | |||
81c520e0dd | |||
c0bda13965 | |||
3b1df75061 | |||
eda5beaed5 | |||
4022670cb1 | |||
7b135a6d0c | |||
bdaa8a06fd | |||
b3f9059452 | |||
917d220623 | |||
ed8ddbc07d | |||
cb01b50fbb | |||
6b3c9d23d0 | |||
3839013a37 | |||
9d5dddab29 | |||
45fa148380 | |||
2ba0063dc0 | |||
b57176d7ef | |||
0dbef6ab9f | |||
8fc4dcfdea | |||
6c98513153 | |||
c3d8ca68b3 | |||
a37692cce4 | |||
a1ad561b98 | |||
7fd0f93d02 | |||
23aaf7f1b7 | |||
41a04e7c67 | |||
77b12b6249 | |||
78fcf579a7 | |||
9ae3bf216e | |||
0e7c56687b | |||
01a80995d3 |
22
README.md
22
README.md
@ -1,7 +1,27 @@
|
||||
# Invidious
|
||||
|
||||
## Invidious is what YouTube should be
|
||||
## Invidious is an alternative front-end to YouTube
|
||||
|
||||
- Audio-only (and no need to keep window open on mobile)
|
||||
- [Open-source](https://github.com/omarroth/invidious) (AGPLv3 licensed)
|
||||
- No ads
|
||||
- No need to create a Google account to save subscriptions
|
||||
- Lightweight (homepage is ~4 KB compressed)
|
||||
- Tools for managing subscriptions:
|
||||
- Only show unseen videos
|
||||
- Only show latest (or latest unseen) video from each channel
|
||||
- Delivers notifications from all subscribed channels
|
||||
- Automatically redirect homepage to feed
|
||||
- Import subscriptions from YouTube
|
||||
- Dark mode
|
||||
- Embed support
|
||||
- Set default player options (speed, quality, autoplay, loop)
|
||||
- Does not require JS to play videos
|
||||
- Support for Reddit comments in place of YT comments
|
||||
- Import/Export subscriptions, watch history, preference
|
||||
- Does not use any of the official YouTube APIs
|
||||
|
||||
Liberapay: https://liberapay.com/omarroth
|
||||
Patreon: https://patreon.com/omarroth
|
||||
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY
|
||||
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk
|
||||
|
@ -171,6 +171,11 @@ div {
|
||||
background-color: rgba(0, 182, 240, 1);
|
||||
}
|
||||
|
||||
/* ProgressBar marker */
|
||||
.vjs-marker {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
/* Big "Play" Button */
|
||||
.video-js .vjs-big-play-button {
|
||||
background-color: rgba(35, 35, 35, 0.5);
|
||||
@ -191,3 +196,23 @@ div {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.video-js .vjs-poster {
|
||||
background-size: cover;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#player {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#player-container {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
height: 0;
|
||||
}
|
||||
|
7
assets/css/grids-responsive-min.css
vendored
Normal file
7
assets/css/grids-responsive-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
assets/css/ionicons.min.css
vendored
Normal file
11
assets/css/ionicons.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
assets/css/pure-min.css
vendored
Normal file
11
assets/css/pure-min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/css/quality-selector.css
Normal file
1
assets/css/quality-selector.css
Normal file
@ -0,0 +1 @@
|
||||
.vjs-quality-selector .vjs-menu-button{margin:0;padding:0;height:100%;width:100%}.vjs-quality-selector .vjs-icon-placeholder{font-family:'VideoJS';font-weight:normal;font-style:normal}.vjs-quality-selector .vjs-icon-placeholder:before{content:'\f110'}.vjs-quality-changing .vjs-big-play-button{display:none}.vjs-quality-changing .vjs-control-bar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;visibility:visible;opacity:1}
|
1
assets/css/video-js.min.css
vendored
Normal file
1
assets/css/video-js.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
assets/css/videojs-share.css
Normal file
7
assets/css/videojs-share.css
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* videojs-share
|
||||
* @version 2.0.1
|
||||
* @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}}
|
1
assets/css/videojs.markers.min.css
vendored
Normal file
1
assets/css/videojs.markers.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
.vjs-marker{position:absolute;left:0;bottom:0;opacity:1;height:100%;transition:opacity .2s ease;-webkit-transition:opacity .2s ease;-moz-transition:opacity .2s ease;z-index:100}.vjs-marker:hover{cursor:pointer;-webkit-transform:scale(1.3,1.3);-moz-transform:scale(1.3,1.3);-o-transform:scale(1.3,1.3);-ms-transform:scale(1.3,1.3);transform:scale(1.3,1.3)}.vjs-tip{visibility:hidden;display:block;opacity:.8;padding:5px;font-size:10px;position:absolute;bottom:14px;z-index:100000}.vjs-tip .vjs-tip-arrow{background:url(data:image/gif;base64,R0lGODlhCQAJAIABAAAAAAAAACH5BAEAAAEALAAAAAAJAAkAAAIRjAOnwIrcDJxvwkplPtchVQAAOw==) no-repeat top left;bottom:0;left:50%;margin-left:-4px;background-position:bottom left;position:absolute;width:9px;height:5px}.vjs-tip .vjs-tip-inner{border-radius:3px;-moz-border-radius:3px;-webkit-border-radius:3px;padding:5px 8px 4px 8px;background-color:#000;color:#fff;max-width:200px;text-align:center}.vjs-break-overlay{visibility:hidden;position:absolute;z-index:100000;top:0}.vjs-break-overlay .vjs-break-overlay-text{padding:9px;text-align:center}
|
BIN
assets/fonts/ionicons.eot
Normal file
BIN
assets/fonts/ionicons.eot
Normal file
Binary file not shown.
2090
assets/fonts/ionicons.svg
Normal file
2090
assets/fonts/ionicons.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 305 KiB |
BIN
assets/fonts/ionicons.ttf
Normal file
BIN
assets/fonts/ionicons.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/ionicons.woff
Normal file
BIN
assets/fonts/ionicons.woff
Normal file
Binary file not shown.
BIN
assets/fonts/ionicons.woff2
Normal file
BIN
assets/fonts/ionicons.woff2
Normal file
Binary file not shown.
4
assets/js/silvermine-videojs-quality-selector.min.js
vendored
Normal file
4
assets/js/silvermine-videojs-quality-selector.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
assets/js/video.min.js
vendored
Normal file
7
assets/js/video.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
assets/js/videojs-contrib-quality-levels.min.js
vendored
Normal file
2
assets/js/videojs-contrib-quality-levels.min.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/*! @name videojs-contrib-quality-levels @version 2.0.7 @license Apache-2.0 */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("video.js"),require("global/document")):"function"==typeof define&&define.amd?define(["video.js","global/document"],t):e.videojsContribQualityLevels=t(e.videojs,e.document)}(this,function(e,t){"use strict";e=e&&e.hasOwnProperty("default")?e.default:e,t=t&&t.hasOwnProperty("default")?t.default:t;var n=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},r=function(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t},i=function(i){function o(){n(this,o);var l=r(this,i.call(this)),s=l;if(e.browser.IS_IE8)for(var u in s=t.createElement("custom"),o.prototype)"constructor"!==u&&(s[u]=o.prototype[u]);return s.levels_=[],s.selectedIndex_=-1,Object.defineProperty(s,"selectedIndex",{get:function(){return s.selectedIndex_}}),Object.defineProperty(s,"length",{get:function(){return s.levels_.length}}),r(l,s)}return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}(o,i),o.prototype.addQualityLevel=function(r){var i=this.getQualityLevelById(r.id);if(i)return i;var o=this.levels_.length;return i=new function r(i){n(this,r);var o=this;if(e.browser.IS_IE8)for(var l in o=t.createElement("custom"),r.prototype)"constructor"!==l&&(o[l]=r.prototype[l]);return o.id=i.id,o.label=o.id,o.width=i.width,o.height=i.height,o.bitrate=i.bandwidth,o.enabled_=i.enabled,Object.defineProperty(o,"enabled",{get:function(){return o.enabled_()},set:function(e){o.enabled_(e)}}),o}(r),""+o in this||Object.defineProperty(this,o,{get:function(){return this.levels_[o]}}),this.levels_.push(i),this.trigger({qualityLevel:i,type:"addqualitylevel"}),i},o.prototype.removeQualityLevel=function(e){for(var t=null,n=0,r=this.length;n<r;n++)if(this[n]===e){t=this.levels_.splice(n,1)[0],this.selectedIndex_===n?this.selectedIndex_=-1:this.selectedIndex_>n&&this.selectedIndex_--;break}return t&&this.trigger({qualityLevel:e,type:"removequalitylevel"}),t},o.prototype.getQualityLevelById=function(e){for(var t=0,n=this.length;t<n;t++){var r=this[t];if(r.id===e)return r}return null},o.prototype.dispose=function(){this.selectedIndex_=-1,this.levels_.length=0},o}(e.EventTarget);for(var o in i.prototype.allowedEvents_={change:"change",addqualitylevel:"addqualitylevel",removequalitylevel:"removequalitylevel"},i.prototype.allowedEvents_)i.prototype["on"+o]=null;var l=function(t){return n=this,e.mergeOptions({},t),r=n.qualityLevels,o=new i,n.on("dispose",function e(){o.dispose(),n.qualityLevels=r,n.off("dispose",e)}),n.qualityLevels=function(){return o},n.qualityLevels.VERSION="__VERSION__",o;var n,r,o};return(e.registerPlugin||e.plugin)("qualityLevels",l),l.VERSION="__VERSION__",l});
|
14
assets/js/videojs-http-streaming.min.js
vendored
Normal file
14
assets/js/videojs-http-streaming.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/js/videojs-markers.min.js
vendored
Normal file
4
assets/js/videojs-markers.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
assets/js/videojs-share.min.js
vendored
Normal file
7
assets/js/videojs-share.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
assets/js/videojs.hotkeys.min.js
vendored
Normal file
2
assets/js/videojs.hotkeys.min.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/* 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})});
|
@ -8,3 +8,4 @@ db:
|
||||
port: 5432
|
||||
dbname: invidious
|
||||
full_refresh: false
|
||||
https_only: false
|
@ -20,6 +20,8 @@ CREATE TABLE public.videos
|
||||
allowed_regions text[] COLLATE pg_catalog."default",
|
||||
is_family_friendly boolean,
|
||||
genre text COLLATE pg_catalog."default",
|
||||
genre_url text COLLATE pg_catalog."default",
|
||||
license text COLLATE pg_catalog."default",
|
||||
CONSTRAINT videos_pkey PRIMARY KEY (id)
|
||||
)
|
||||
WITH (
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: invidious
|
||||
version: 0.2.0
|
||||
version: 0.4.0
|
||||
|
||||
authors:
|
||||
- Omar Roth <omarroth@hotmail.com>
|
||||
@ -9,13 +9,13 @@ targets:
|
||||
main: src/invidious.cr
|
||||
|
||||
dependencies:
|
||||
detect_language:
|
||||
github: detectlanguage/detectlanguage-crystal
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
pg:
|
||||
github: will/crystal-pg
|
||||
detect_language:
|
||||
github: detectlanguage/detectlanguage-crystal
|
||||
|
||||
crystal: 0.26.0
|
||||
crystal: 0.26.1
|
||||
|
||||
license: AGPLv3
|
||||
|
728
src/invidious.cr
728
src/invidious.cr
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,13 @@ def fetch_channel(ucid, client, db, pull_all_videos = true)
|
||||
end
|
||||
author = author.content
|
||||
|
||||
# Auto-generated channels
|
||||
# https://support.google.com/youtube/answer/2579942
|
||||
if author.ends_with?(" - Topic") ||
|
||||
{"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? author
|
||||
auto_generated = true
|
||||
end
|
||||
|
||||
if !pull_all_videos
|
||||
rss.xpath_nodes("//feed/entry").each do |entry|
|
||||
video_id = entry.xpath_node("videoid").not_nil!.content
|
||||
@ -69,64 +76,103 @@ def fetch_channel(ucid, client, db, pull_all_videos = true)
|
||||
updated = $4, ucid = $5, author = $6", video_array)
|
||||
end
|
||||
else
|
||||
videos = [] of ChannelVideo
|
||||
page = 1
|
||||
ids = [] of String
|
||||
|
||||
loop do
|
||||
url = produce_videos_url(ucid, page)
|
||||
url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated)
|
||||
response = client.get(url)
|
||||
|
||||
json = JSON.parse(response.body)
|
||||
content_html = json["content_html"].as_s
|
||||
if content_html.empty?
|
||||
# If we don't get anything, move on
|
||||
|
||||
if json["content_html"]? && !json["content_html"].as_s.empty?
|
||||
document = XML.parse_html(json["content_html"].as_s)
|
||||
nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")]))
|
||||
else
|
||||
break
|
||||
end
|
||||
document = XML.parse_html(content_html)
|
||||
|
||||
document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])).each do |item|
|
||||
anchor = item.xpath_node(%q(.//h3[contains(@class,"yt-lockup-title")]/a))
|
||||
if !anchor
|
||||
raise "could not find anchor"
|
||||
if auto_generated
|
||||
videos = extract_videos(nodeset)
|
||||
else
|
||||
videos = extract_videos(nodeset, ucid)
|
||||
videos.each { |video| video.ucid = ucid }
|
||||
videos.each { |video| video.author = author }
|
||||
end
|
||||
|
||||
title = anchor.content.strip
|
||||
video_id = anchor["href"].lchop("/watch?v=")
|
||||
count = nodeset.size
|
||||
videos = videos.map { |video| ChannelVideo.new(video.id, video.title, video.published, Time.now, video.ucid, video.author) }
|
||||
|
||||
published = item.xpath_node(%q(.//div[@class="yt-lockup-meta"]/ul/li[1]))
|
||||
if !published
|
||||
# This happens on Youtube red videos, here we just skip them
|
||||
next
|
||||
end
|
||||
published = published.content
|
||||
published = decode_date(published)
|
||||
videos.each do |video|
|
||||
ids << video.id
|
||||
db.exec("UPDATE users SET notifications = notifications || $1 \
|
||||
WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, video.ucid)
|
||||
|
||||
videos << ChannelVideo.new(video_id, title, published, Time.now, ucid, author)
|
||||
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", video_array)
|
||||
end
|
||||
|
||||
if document.xpath_nodes(%q(//li[contains(@class, "channels-content-item")])).size < 30
|
||||
if count < 30
|
||||
break
|
||||
end
|
||||
|
||||
page += 1
|
||||
end
|
||||
|
||||
video_ids = [] of String
|
||||
videos.each do |video|
|
||||
db.exec("UPDATE users SET notifications = notifications || $1 \
|
||||
WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, ucid)
|
||||
video_ids << video.id
|
||||
|
||||
video_array = video.to_a
|
||||
args = arg_array(video_array)
|
||||
db.exec("INSERT INTO channel_videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", video_array)
|
||||
end
|
||||
|
||||
# When a video is deleted from a channel, we find and remove it here
|
||||
db.exec("DELETE FROM channel_videos * WHERE NOT id = ANY ('{#{video_ids.map { |a| %("#{a}") }.join(",")}}') AND ucid = $1", ucid)
|
||||
db.exec("DELETE FROM channel_videos * WHERE NOT id = ANY ('{#{ids.map { |id| %("#{id}") }.join(",")}}') AND ucid = $1", ucid)
|
||||
end
|
||||
|
||||
channel = InvidiousChannel.new(ucid, author, Time.now)
|
||||
|
||||
return channel
|
||||
end
|
||||
|
||||
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil)
|
||||
if auto_generated
|
||||
seed = Time.epoch(1525757349)
|
||||
|
||||
until seed >= Time.now
|
||||
seed += 1.month
|
||||
end
|
||||
timestamp = seed - (page - 1).months
|
||||
|
||||
page = "#{timestamp.epoch}"
|
||||
switch = "\x36"
|
||||
else
|
||||
page = "#{page}"
|
||||
switch = "\x00"
|
||||
end
|
||||
|
||||
meta = "\x12\x06videos"
|
||||
meta += "\x30\x02"
|
||||
meta += "\x38\x01"
|
||||
meta += "\x60\x01"
|
||||
meta += "\x6a\x00"
|
||||
meta += "\xb8\x01\x00"
|
||||
meta += "\x20#{switch}"
|
||||
meta += "\x7a"
|
||||
meta += page.size.to_u8.unsafe_chr
|
||||
meta += page
|
||||
|
||||
meta = Base64.urlsafe_encode(meta)
|
||||
meta = URI.escape(meta)
|
||||
|
||||
continuation = "\x12"
|
||||
continuation += ucid.size.to_u8.unsafe_chr
|
||||
continuation += ucid
|
||||
continuation += "\x1a"
|
||||
continuation += meta.size.to_u8.unsafe_chr
|
||||
continuation += meta
|
||||
|
||||
continuation = continuation.size.to_u8.unsafe_chr + continuation
|
||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}"
|
||||
|
||||
return url
|
||||
end
|
||||
|
@ -93,17 +93,19 @@ def template_youtube_comments(comments)
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
||||
onclick="load_comments(this)">View #{child["replies"]["replyCount"]} replies</a>
|
||||
onclick="get_youtube_replies(this)">View #{child["replies"]["replyCount"]} replies</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).full_path}"
|
||||
|
||||
html += <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-2-24">
|
||||
<img style="width:90%; padding-right:1em; padding-top:1em;" src="#{child["authorThumbnails"][-1]["url"]}">
|
||||
<img style="width:90%; padding-right:1em; padding-top:1em;" src="#{author_thumbnail}">
|
||||
</div>
|
||||
<div class="pure-u-22-24">
|
||||
<p>
|
||||
@ -113,7 +115,7 @@ def template_youtube_comments(comments)
|
||||
- #{recode_date(Time.epoch(child["published"].as_i64))} ago
|
||||
</p>
|
||||
<div>
|
||||
#{child["content"]}
|
||||
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
@ -127,7 +129,7 @@ def template_youtube_comments(comments)
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||
onclick="load_comments(this)">Load more</a>
|
||||
onclick="get_youtube_replies(this)">Load more</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -190,37 +192,19 @@ def template_reddit_comments(root)
|
||||
return html
|
||||
end
|
||||
|
||||
def add_alt_links(html)
|
||||
alt_links = [] of {String, String}
|
||||
def replace_links(html)
|
||||
html = XML.parse_html(html)
|
||||
|
||||
# This is painful but likely the only way to accomplish this in Crystal,
|
||||
# as Crystigiri and others are not able to insert XML Nodes into a document.
|
||||
# The goal here is to use as little regex as possible
|
||||
html.scan(/<a[^>]*>([^<]+)<\/a>/) do |match|
|
||||
anchor = XML.parse_html(match[0])
|
||||
anchor = anchor.xpath_node("//a").not_nil!
|
||||
html.xpath_nodes(%q(//a)).each do |anchor|
|
||||
url = URI.parse(anchor["href"])
|
||||
|
||||
if ["www.youtube.com", "m.youtube.com"].includes?(url.host)
|
||||
if {"www.youtube.com", "m.youtube.com", "youtu.be"}.includes?(url.host)
|
||||
if url.path == "/redirect"
|
||||
params = HTTP::Params.parse(url.query.not_nil!)
|
||||
alt_url = params["q"]?
|
||||
alt_url ||= "/"
|
||||
anchor["href"] = params["q"]?
|
||||
else
|
||||
alt_url = url.full_path
|
||||
anchor["href"] = url.full_path
|
||||
end
|
||||
|
||||
alt_link = <<-END_HTML
|
||||
<a href="#{alt_url}">
|
||||
<i class="icon ion-ios-link"></i>
|
||||
</a>
|
||||
END_HTML
|
||||
elsif url.host == "youtu.be"
|
||||
alt_link = <<-END_HTML
|
||||
<a href="/watch?v=#{url.path.try &.lchop("/")}&#{url.query}">
|
||||
<i class="icon ion-ios-link"></i>
|
||||
</a>
|
||||
END_HTML
|
||||
elsif url.to_s == "#"
|
||||
begin
|
||||
length_seconds = decode_length_seconds(anchor.content)
|
||||
@ -228,23 +212,12 @@ def add_alt_links(html)
|
||||
length_seconds = decode_time(anchor.content)
|
||||
end
|
||||
|
||||
alt_anchor = <<-END_HTML
|
||||
<a href="javascript:void(0)" onclick="player.currentTime(#{length_seconds})">#{anchor.content}</a>
|
||||
END_HTML
|
||||
|
||||
html = html.sub(anchor.to_s, alt_anchor)
|
||||
next
|
||||
else
|
||||
alt_link = ""
|
||||
end
|
||||
|
||||
alt_links << {anchor.to_s, alt_link}
|
||||
end
|
||||
|
||||
alt_links.each do |original, alternate|
|
||||
html = html.sub(original, original + alternate)
|
||||
anchor["href"] = "javascript:void(0)"
|
||||
anchor["onclick"] = "player.currentTime(#{length_seconds})"
|
||||
end
|
||||
end
|
||||
|
||||
html = html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
return html
|
||||
end
|
||||
|
||||
@ -267,5 +240,5 @@ def fill_links(html, scheme, host)
|
||||
html = html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
end
|
||||
|
||||
html
|
||||
return html
|
||||
end
|
||||
|
@ -18,7 +18,7 @@ class Config
|
||||
end
|
||||
|
||||
class FilteredCompressHandler < Kemal::Handler
|
||||
exclude ["/videoplayback", "/videoplayback/*", "/api/*"]
|
||||
exclude ["/videoplayback", "/videoplayback/*", "/vi/*", "/api/*", "/ggpht/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env if exclude_match? env
|
||||
@ -41,6 +41,17 @@ class FilteredCompressHandler < Kemal::Handler
|
||||
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
|
||||
|
||||
def rank_videos(db, n, filter, url)
|
||||
top = [] of {Float64, String}
|
||||
|
||||
@ -116,81 +127,6 @@ def login_req(login_form, f_req)
|
||||
return HTTP::Params.encode(data)
|
||||
end
|
||||
|
||||
def produce_videos_url(ucid, page = 1)
|
||||
page = "#{page}"
|
||||
|
||||
meta = "\x12\x06videos \x00\x30\x02\x38\x01\x60\x01\x6a\x00\x7a"
|
||||
meta += page.size.to_u8.unsafe_chr
|
||||
meta += page
|
||||
meta += "\xb8\x01\x00"
|
||||
|
||||
meta = Base64.urlsafe_encode(meta)
|
||||
meta = URI.escape(meta)
|
||||
|
||||
continuation = "\x12"
|
||||
continuation += ucid.size.to_u8.unsafe_chr
|
||||
continuation += ucid
|
||||
continuation += "\x1a"
|
||||
continuation += meta.size.to_u8.unsafe_chr
|
||||
continuation += meta
|
||||
|
||||
continuation = continuation.size.to_u8.unsafe_chr + continuation
|
||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}"
|
||||
|
||||
return url
|
||||
end
|
||||
|
||||
def read_var_int(bytes)
|
||||
numRead = 0
|
||||
result = 0
|
||||
|
||||
read = bytes[numRead]
|
||||
|
||||
if bytes.size == 1
|
||||
result = bytes[0].to_i32
|
||||
else
|
||||
while ((read & 0b10000000) != 0)
|
||||
read = bytes[numRead].to_u64
|
||||
value = (read & 0b01111111)
|
||||
result |= (value << (7 * numRead))
|
||||
|
||||
numRead += 1
|
||||
if numRead > 5
|
||||
raise "VarInt is too big"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def write_var_int(value : Int)
|
||||
bytes = [] of UInt8
|
||||
value = value.to_u32
|
||||
|
||||
if value == 0
|
||||
bytes = [0_u8]
|
||||
else
|
||||
while value != 0
|
||||
temp = (value & 0b01111111).to_u8
|
||||
value = value >> 7
|
||||
|
||||
if value != 0
|
||||
temp |= 0b10000000
|
||||
end
|
||||
|
||||
bytes << temp
|
||||
end
|
||||
end
|
||||
|
||||
return bytes
|
||||
end
|
||||
|
||||
def generate_captcha(key)
|
||||
minute = Random::Secure.rand(12)
|
||||
minute_angle = minute * 30
|
||||
@ -240,7 +176,7 @@ def generate_captcha(key)
|
||||
return {challenge: challenge, token: token}
|
||||
end
|
||||
|
||||
def html_to_description(description_html)
|
||||
def html_to_content(description_html)
|
||||
if !description_html
|
||||
description = ""
|
||||
description_html = ""
|
||||
@ -248,10 +184,15 @@ def html_to_description(description_html)
|
||||
description_html = description_html.to_s
|
||||
description = description_html.gsub("<br>", "\n")
|
||||
description = description.gsub("<br/>", "\n")
|
||||
|
||||
if description.empty?
|
||||
description = ""
|
||||
else
|
||||
description = XML.parse_html(description).content.strip("\n ")
|
||||
end
|
||||
end
|
||||
|
||||
return description, description_html
|
||||
return description_html, description
|
||||
end
|
||||
|
||||
def extract_videos(nodeset, ucid = nil)
|
||||
@ -319,7 +260,7 @@ def extract_videos(nodeset, ucid = nil)
|
||||
view_count ||= 0_i64
|
||||
|
||||
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")]))
|
||||
description, description_html = html_to_description(description_html)
|
||||
description_html, description = html_to_content(description_html)
|
||||
|
||||
length_seconds = node.xpath_node(%q(.//span[@class="video-time"]))
|
||||
if length_seconds
|
||||
|
@ -169,7 +169,7 @@ def get_referer(env, fallback = "/")
|
||||
|
||||
referer = URI.parse(referer)
|
||||
|
||||
# "Unroll" nested referers
|
||||
# "Unroll" nested referrers
|
||||
loop do
|
||||
if referer.query
|
||||
params = HTTP::Params.parse(referer.query.not_nil!)
|
||||
@ -184,6 +184,7 @@ def get_referer(env, fallback = "/")
|
||||
end
|
||||
|
||||
referer = referer.full_path
|
||||
referer = "/" + referer.lstrip("\/\\")
|
||||
|
||||
if referer == env.request.path
|
||||
referer = fallback
|
||||
@ -191,3 +192,49 @@ def get_referer(env, fallback = "/")
|
||||
|
||||
return referer
|
||||
end
|
||||
|
||||
def read_var_int(bytes)
|
||||
numRead = 0
|
||||
result = 0
|
||||
|
||||
read = bytes[numRead]
|
||||
|
||||
if bytes.size == 1
|
||||
result = bytes[0].to_i32
|
||||
else
|
||||
while ((read & 0b10000000) != 0)
|
||||
read = bytes[numRead].to_u64
|
||||
value = (read & 0b01111111)
|
||||
result |= (value << (7 * numRead))
|
||||
|
||||
numRead += 1
|
||||
if numRead > 5
|
||||
raise "VarInt is too big"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
def write_var_int(value : Int)
|
||||
bytes = [] of UInt8
|
||||
value = value.to_u32
|
||||
|
||||
if value == 0
|
||||
bytes = [0_u8]
|
||||
else
|
||||
while value != 0
|
||||
temp = (value & 0b01111111).to_u8
|
||||
value = value >> 7
|
||||
|
||||
if value != 0
|
||||
temp |= 0b10000000
|
||||
end
|
||||
|
||||
bytes << temp
|
||||
end
|
||||
end
|
||||
|
||||
return bytes
|
||||
end
|
||||
|
@ -2,13 +2,13 @@ def crawl_videos(db)
|
||||
ids = Deque(String).new
|
||||
random = Random.new
|
||||
|
||||
search(random.base64(3)).each do |video|
|
||||
search(random.base64(3)).as(Tuple)[1].each do |video|
|
||||
ids << video.id
|
||||
end
|
||||
|
||||
loop do
|
||||
if ids.empty?
|
||||
search(random.base64(3)).each do |video|
|
||||
search(random.base64(3)).as(Tuple)[1].each do |video|
|
||||
ids << video.id
|
||||
end
|
||||
end
|
||||
@ -141,8 +141,7 @@ end
|
||||
def update_decrypt_function
|
||||
loop do
|
||||
begin
|
||||
client = make_client(YT_URL)
|
||||
decrypt_function = fetch_decrypt_function(client)
|
||||
decrypt_function = fetch_decrypt_function
|
||||
rescue ex
|
||||
next
|
||||
end
|
||||
|
@ -5,6 +5,7 @@ class Playlist
|
||||
author: String,
|
||||
ucid: String,
|
||||
description: String,
|
||||
description_html: String,
|
||||
video_count: Int32,
|
||||
views: Int64,
|
||||
updated: Time,
|
||||
@ -87,28 +88,29 @@ def produce_playlist_url(id, index)
|
||||
end
|
||||
ucid = "VL" + id
|
||||
|
||||
continuation = [0x08_u8] + write_var_int(index)
|
||||
slice = continuation.to_unsafe.to_slice(continuation.size)
|
||||
slice = Base64.urlsafe_encode(slice, false)
|
||||
meta = "\x08#{write_var_int(index).join}"
|
||||
meta = Base64.urlsafe_encode(meta, false)
|
||||
meta = "PT:#{meta}"
|
||||
|
||||
# Inner Base64
|
||||
continuation = "PT:" + slice
|
||||
continuation = [0x7a_u8, continuation.bytes.size.to_u8] + continuation.bytes
|
||||
slice = continuation.to_unsafe.to_slice(continuation.size)
|
||||
slice = Base64.urlsafe_encode(slice)
|
||||
slice = URI.escape(slice)
|
||||
wrapped = "\x7a"
|
||||
wrapped += meta.bytes.size.unsafe_chr
|
||||
wrapped += meta
|
||||
|
||||
# Outer Base64
|
||||
continuation = [0x1a.to_u8, slice.bytes.size.to_u8] + slice.bytes
|
||||
continuation = ucid.bytes + continuation
|
||||
continuation = [0x12_u8, ucid.size.to_u8] + continuation
|
||||
continuation = [0xe2_u8, 0xa9_u8, 0x85_u8, 0xb2_u8, 2_u8, continuation.size.to_u8] + continuation
|
||||
wrapped = Base64.urlsafe_encode(wrapped)
|
||||
meta = URI.escape(wrapped)
|
||||
|
||||
# Wrap bytes
|
||||
slice = continuation.to_unsafe.to_slice(continuation.size)
|
||||
slice = Base64.urlsafe_encode(slice)
|
||||
slice = URI.escape(slice)
|
||||
continuation = slice
|
||||
continuation = "\x12"
|
||||
continuation += ucid.size.unsafe_chr
|
||||
continuation += ucid
|
||||
continuation += "\x1a"
|
||||
continuation += meta.bytes.size.unsafe_chr
|
||||
continuation += meta
|
||||
|
||||
continuation = continuation.size.to_u8.unsafe_chr + continuation
|
||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?action_continuation=1&continuation=#{continuation}"
|
||||
|
||||
@ -118,22 +120,18 @@ end
|
||||
def fetch_playlist(plid)
|
||||
client = make_client(YT_URL)
|
||||
response = client.get("/playlist?list=#{plid}&disable_polymer=1")
|
||||
document = XML.parse_html(response.body)
|
||||
body = response.body.gsub(<<-END_BUTTON
|
||||
<button class="yt-uix-button yt-uix-button-size-default yt-uix-button-link yt-uix-expander-head playlist-description-expander yt-uix-inlineedit-ignore-edit" type="button" onclick=";return false;"><span class="yt-uix-button-content"> less <img alt="" src="/yts/img/pixel-vfl3z5WfW.gif">
|
||||
</span></button>
|
||||
END_BUTTON
|
||||
, "")
|
||||
document = XML.parse_html(body)
|
||||
|
||||
title = document.xpath_node(%q(//h1[@class="pl-header-title"])).not_nil!.content
|
||||
title = title.strip(" \n")
|
||||
|
||||
description = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1]))
|
||||
description ||= document.xpath_node(%q(//span[@class="pl-header-description-text"]))
|
||||
|
||||
if description
|
||||
description = description.to_xml.strip(" \n")
|
||||
description = description.split("<button ")[0]
|
||||
description = fill_links(description, "https", "www.youtube.com")
|
||||
description = add_alt_links(description)
|
||||
else
|
||||
description = ""
|
||||
end
|
||||
description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1]))
|
||||
description_html, description = html_to_content(description_html)
|
||||
|
||||
anchor = document.xpath_node(%q(//ul[@class="pl-header-details"])).not_nil!
|
||||
author = anchor.xpath_node(%q(.//li[1]/a)).not_nil!.content
|
||||
@ -151,6 +149,7 @@ def fetch_playlist(plid)
|
||||
author,
|
||||
ucid,
|
||||
description,
|
||||
description_html,
|
||||
video_count,
|
||||
views,
|
||||
updated
|
||||
|
@ -12,33 +12,75 @@ class SearchVideo
|
||||
})
|
||||
end
|
||||
|
||||
def search(query, page = 1, search_params = build_search_params(content_type: "video"))
|
||||
def channel_search(query, page, channel)
|
||||
client = make_client(YT_URL)
|
||||
|
||||
response = client.get("/user/#{channel}")
|
||||
document = XML.parse_html(response.body)
|
||||
canonical = document.xpath_node(%q(//link[@rel="canonical"]))
|
||||
|
||||
if !canonical
|
||||
response = client.get("/channel/#{channel}")
|
||||
document = XML.parse_html(response.body)
|
||||
canonical = document.xpath_node(%q(//link[@rel="canonical"]))
|
||||
end
|
||||
|
||||
if !canonical
|
||||
return 0, [] of SearchVideo
|
||||
end
|
||||
|
||||
ucid = canonical["href"].split("/")[-1]
|
||||
|
||||
url = produce_channel_search_url(ucid, query, page)
|
||||
response = client.get(url)
|
||||
json = JSON.parse(response.body)
|
||||
|
||||
if json["content_html"]? && !json["content_html"].as_s.empty?
|
||||
document = XML.parse_html(json["content_html"].as_s)
|
||||
nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")]))
|
||||
|
||||
count = nodeset.size
|
||||
videos = extract_videos(nodeset)
|
||||
else
|
||||
count = 0
|
||||
videos = [] of SearchVideo
|
||||
end
|
||||
|
||||
return count, videos
|
||||
end
|
||||
|
||||
def search(query, page = 1, search_params = produce_search_params(content_type: "video"))
|
||||
client = make_client(YT_URL)
|
||||
if query.empty?
|
||||
return {0, [] of SearchVideo}
|
||||
end
|
||||
|
||||
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}&disable_polymer=1").body
|
||||
if html.empty?
|
||||
return [] of SearchVideo
|
||||
return {0, [] of SearchVideo}
|
||||
end
|
||||
|
||||
html = XML.parse_html(html)
|
||||
nodeset = html.xpath_nodes(%q(//ol[@class="item-section"]/li))
|
||||
videos = extract_videos(nodeset)
|
||||
|
||||
return videos
|
||||
return {nodeset.size, videos}
|
||||
end
|
||||
|
||||
def build_search_params(sort_by = "relevance", date : String = "", content_type : String = "", duration : String = "", features : Array(String) = [] of String)
|
||||
def produce_search_params(sort : String = "relevance", date : String = "", content_type : String = "",
|
||||
duration : String = "", features : Array(String) = [] of String)
|
||||
head = "\x08"
|
||||
head += case sort_by
|
||||
head += case sort
|
||||
when "relevance"
|
||||
"\x00"
|
||||
when "rating"
|
||||
"\x01"
|
||||
when "upload_date"
|
||||
when "upload_date", "date"
|
||||
"\x02"
|
||||
when "view_count"
|
||||
when "view_count", "views"
|
||||
"\x03"
|
||||
else
|
||||
raise "No sort #{sort_by}"
|
||||
raise "No sort #{sort}"
|
||||
end
|
||||
|
||||
body = ""
|
||||
@ -87,7 +129,7 @@ def build_search_params(sort_by = "relevance", date : String = "", content_type
|
||||
"\x20\x01"
|
||||
when "subtitles"
|
||||
"\x28\x01"
|
||||
when "creative_commons"
|
||||
when "creative_commons", "cc"
|
||||
"\x30\x01"
|
||||
when "3d"
|
||||
"\x38\x01"
|
||||
@ -109,7 +151,7 @@ def build_search_params(sort_by = "relevance", date : String = "", content_type
|
||||
end
|
||||
|
||||
if body.size > 0
|
||||
token = head + "\x12" + body.size.to_u8.unsafe_chr + body
|
||||
token = head + "\x12" + body.size.unsafe_chr + body
|
||||
else
|
||||
token = head
|
||||
end
|
||||
@ -119,3 +161,40 @@ def build_search_params(sort_by = "relevance", date : String = "", content_type
|
||||
|
||||
return token
|
||||
end
|
||||
|
||||
def produce_channel_search_url(ucid, query, page)
|
||||
page = "#{page}"
|
||||
|
||||
meta = "\x12\x06search"
|
||||
meta += "\x30\x02"
|
||||
meta += "\x38\x01"
|
||||
meta += "\x60\x01"
|
||||
meta += "\x6a\x00"
|
||||
meta += "\xb8\x01\x00"
|
||||
meta += "\x7a"
|
||||
meta += page.size.unsafe_chr
|
||||
meta += page
|
||||
|
||||
meta = Base64.urlsafe_encode(meta)
|
||||
meta = URI.escape(meta)
|
||||
|
||||
continuation = "\x12"
|
||||
continuation += ucid.size.unsafe_chr
|
||||
continuation += ucid
|
||||
continuation += "\x1a"
|
||||
continuation += meta.size.unsafe_chr
|
||||
continuation += meta
|
||||
continuation += "\x5a"
|
||||
continuation += query.size.unsafe_chr
|
||||
continuation += query
|
||||
|
||||
continuation = continuation.size.unsafe_chr + continuation
|
||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation)
|
||||
continuation = URI.escape(continuation)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}"
|
||||
|
||||
return url
|
||||
end
|
||||
|
@ -1,25 +1,25 @@
|
||||
def fetch_decrypt_function(client, id = "CvFH_6DNRCY")
|
||||
def fetch_decrypt_function(id = "CvFH_6DNRCY")
|
||||
client = make_client(YT_URL)
|
||||
document = client.get("/watch?v=#{id}").body
|
||||
url = document.match(/src="(?<url>\/yts\/jsbin\/player-.{9}\/en_US\/base.js)"/).not_nil!["url"]
|
||||
player = client.get(url).body
|
||||
|
||||
function_name = player.match(/\(b\|\|\(b="signature"\),d.set\(b,(?<name>[a-zA-Z0-9]{2})\(c\)\)\)/).not_nil!["name"]
|
||||
function_body = player.match(/#{function_name}=function\(a\){(?<body>[^}]+)}/).not_nil!["body"]
|
||||
function_name = player.match(/^(?<name>[^=]+)=function\(a\){a=a\.split\(""\)/m).not_nil!["name"]
|
||||
function_body = player.match(/^#{function_name}=function\(a\){(?<body>[^}]+)}/m).not_nil!["body"]
|
||||
function_body = function_body.split(";")[1..-2]
|
||||
|
||||
var_name = function_body[0][0, 2]
|
||||
var_body = player.delete("\n").match(/var #{var_name}={(?<body>(.*?))};/).not_nil!["body"]
|
||||
|
||||
operations = {} of String => String
|
||||
matches = player.delete("\n").match(/var #{var_name}={(?<op1>[a-zA-Z0-9]{2}:[^}]+}),(?<op2>[a-zA-Z0-9]{2}:[^}]+}),(?<op3>[a-zA-Z0-9]{2}:[^}]+})};/).not_nil!
|
||||
3.times do |i|
|
||||
operation = matches["op#{i + 1}"]
|
||||
op_name = operation[0, 2]
|
||||
var_body.split("},").each do |operation|
|
||||
op_name = operation.match(/^[^:]+/).not_nil![0]
|
||||
op_body = operation.match(/\{[^}]+/).not_nil![0]
|
||||
|
||||
op_body = operation.match(/\{[^}]+\}/).not_nil![0]
|
||||
case op_body
|
||||
when "{a.reverse()}"
|
||||
when "{a.reverse()"
|
||||
operations[op_name] = "a"
|
||||
when "{a.splice(0,b)}"
|
||||
when "{a.splice(0,b)"
|
||||
operations[op_name] = "b"
|
||||
else
|
||||
operations[op_name] = "c"
|
||||
@ -28,11 +28,10 @@ def fetch_decrypt_function(client, id = "CvFH_6DNRCY")
|
||||
|
||||
decrypt_function = [] of {name: String, value: Int32}
|
||||
function_body.each do |function|
|
||||
function = function.lchop(var_name + ".")
|
||||
op_name = function[0, 2]
|
||||
function = function.lchop(var_name).delete("[].")
|
||||
|
||||
function = function.lchop(op_name + "(a,")
|
||||
value = function.rchop(")").to_i
|
||||
op_name = function.match(/[^\(]+/).not_nil![0]
|
||||
value = function.match(/\(a,(?<value>[\d]+)\)/).not_nil!["value"].to_i
|
||||
|
||||
decrypt_function << {name: operations[op_name], value: value}
|
||||
end
|
||||
|
@ -32,8 +32,9 @@ DEFAULT_USER_PREFERENCES = Preferences.from_json({
|
||||
"speed" => 1.0,
|
||||
"quality" => "hd720",
|
||||
"volume" => 100,
|
||||
"comments" => "youtube",
|
||||
"comments" => ["youtube", ""],
|
||||
"captions" => ["", "", ""],
|
||||
"related_videos" => true,
|
||||
"dark_mode" => false,
|
||||
"thin_mode " => false,
|
||||
"max_results" => 40,
|
||||
@ -43,6 +44,29 @@ DEFAULT_USER_PREFERENCES = Preferences.from_json({
|
||||
}.to_json)
|
||||
|
||||
class Preferences
|
||||
module StringToArray
|
||||
def self.to_json(value : Array(String), json : JSON::Builder)
|
||||
json.array do
|
||||
value.each do |element|
|
||||
json.string element
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_json(value : JSON::PullParser) : Array(String)
|
||||
begin
|
||||
result = [] of String
|
||||
value.read_array do
|
||||
result << value.read_string
|
||||
end
|
||||
rescue ex
|
||||
result = [value.read_string, ""]
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
JSON.mapping({
|
||||
video_loop: Bool,
|
||||
autoplay: Bool,
|
||||
@ -50,8 +74,9 @@ class Preferences
|
||||
quality: String,
|
||||
volume: Int32,
|
||||
comments: {
|
||||
type: String,
|
||||
default: "youtube",
|
||||
type: Array(String),
|
||||
default: ["youtube", ""],
|
||||
converter: StringToArray,
|
||||
},
|
||||
captions: {
|
||||
type: Array(String),
|
||||
@ -61,6 +86,10 @@ class Preferences
|
||||
type: Bool,
|
||||
default: false,
|
||||
},
|
||||
related_videos: {
|
||||
type: Bool,
|
||||
default: true,
|
||||
},
|
||||
dark_mode: Bool,
|
||||
thin_mode: {
|
||||
type: Bool,
|
||||
@ -111,7 +140,7 @@ def fetch_user(sid, client, headers, db)
|
||||
|
||||
channels = [] of String
|
||||
feed.xpath_nodes(%q(//ul[@id="guide-channels"]/li/a)).each do |channel|
|
||||
if !["Popular on YouTube", "Music", "Sports", "Gaming"].includes? channel["title"]
|
||||
if !{"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? channel["title"]
|
||||
channel_id = channel["href"].lstrip("/channel/")
|
||||
|
||||
begin
|
||||
|
@ -112,11 +112,11 @@ REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "A
|
||||
BYPASS_REGIONS = {"CA", "DE", "FR", "JP", "RU", "UK"}
|
||||
|
||||
VIDEO_THUMBNAILS = {
|
||||
{name: "default", url: "default", height: 90, width: 120},
|
||||
{name: "medium", url: "mqdefault", height: 180, width: 320},
|
||||
{name: "high", url: "hqdefault", height: 360, width: 480},
|
||||
{name: "maxresdefault", url: "maxresdefault", height: 720, width: 1280},
|
||||
{name: "sddefault", url: "sddefault", height: 480, width: 640},
|
||||
{name: "maxresdefault", url: "maxresdefault", height: 1280, width: 720},
|
||||
{name: "high", url: "hqdefault", height: 360, width: 480},
|
||||
{name: "medium", url: "mqdefault", height: 180, width: 320},
|
||||
{name: "default", url: "default", height: 90, width: 120},
|
||||
{name: "start", url: "1", height: 90, width: 120},
|
||||
{name: "middle", url: "2", height: 90, width: 120},
|
||||
{name: "end", url: "3", height: 90, width: 120},
|
||||
@ -258,10 +258,82 @@ class Video
|
||||
|
||||
def adaptive_fmts(decrypt_function)
|
||||
adaptive_fmts = [] of HTTP::Params
|
||||
|
||||
if self.info.has_key?("adaptive_fmts")
|
||||
self.info["adaptive_fmts"].split(",") do |string|
|
||||
adaptive_fmts << HTTP::Params.parse(string)
|
||||
end
|
||||
elsif self.info.has_key?("dashmpd")
|
||||
client = make_client(YT_URL)
|
||||
response = client.get(self.info["dashmpd"])
|
||||
document = XML.parse_html(response.body)
|
||||
|
||||
document.xpath_nodes(%q(//adaptationset)).each do |adaptation_set|
|
||||
mime_type = adaptation_set["mimetype"]
|
||||
|
||||
document.xpath_nodes(%q(.//representation)).each do |representation|
|
||||
codecs = representation["codecs"]
|
||||
itag = representation["id"]
|
||||
bandwidth = representation["bandwidth"]
|
||||
url = representation.xpath_node(%q(.//baseurl)).not_nil!.content
|
||||
|
||||
clen = url.match(/clen\/(?<clen>\d+)/).try &.["clen"]
|
||||
clen ||= "0"
|
||||
lmt = url.match(/lmt\/(?<lmt>\d+)/).try &.["lmt"]
|
||||
lmt ||= "#{((Time.now + 1.hour).epoch_f.to_f64 * 1000000).to_i64}"
|
||||
|
||||
segment_list = representation.xpath_node(%q(.//segmentlist)).not_nil!
|
||||
init = segment_list.xpath_node(%q(.//initialization))
|
||||
|
||||
# TODO: Replace with sane defaults when byteranges are absent
|
||||
if init && !init["sourceurl"].starts_with? "sq"
|
||||
init = init["sourceurl"].lchop("range/")
|
||||
|
||||
index = segment_list.xpath_node(%q(.//segmenturl)).not_nil!["media"]
|
||||
index = index.lchop("range/")
|
||||
index = "#{init.split("-")[1].to_i + 1}-#{index.split("-")[0].to_i}"
|
||||
else
|
||||
init = "0-0"
|
||||
index = "1-1"
|
||||
end
|
||||
|
||||
params = {
|
||||
"type" => ["#{mime_type}; codecs=\"#{codecs}\""],
|
||||
"url" => [url],
|
||||
"projection_type" => ["1"],
|
||||
"index" => [index],
|
||||
"init" => [init],
|
||||
"xtags" => [] of String,
|
||||
"lmt" => [lmt],
|
||||
"clen" => [clen],
|
||||
"bitrate" => [bandwidth],
|
||||
"itag" => [itag],
|
||||
}
|
||||
|
||||
if mime_type == "video/mp4"
|
||||
width = representation["width"]?
|
||||
height = representation["height"]?
|
||||
fps = representation["framerate"]?
|
||||
|
||||
metadata = itag_to_metadata?(itag)
|
||||
if metadata
|
||||
width ||= metadata["width"]?
|
||||
height ||= metadata["height"]?
|
||||
fps ||= metadata["fps"]?
|
||||
end
|
||||
|
||||
if width && height
|
||||
params["size"] = ["#{width}x#{height}"]
|
||||
end
|
||||
|
||||
if width
|
||||
params["quality_label"] = ["#{height}p"]
|
||||
end
|
||||
end
|
||||
|
||||
adaptive_fmts << HTTP::Params.new(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if adaptive_fmts[0]? && adaptive_fmts[0]["s"]?
|
||||
@ -345,6 +417,11 @@ class Video
|
||||
allowed_regions: Array(String),
|
||||
is_family_friendly: Bool,
|
||||
genre: String,
|
||||
genre_url: String,
|
||||
license: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
@ -366,15 +443,17 @@ def get_video(id, db, refresh = true)
|
||||
if db.query_one?("SELECT EXISTS (SELECT true FROM videos WHERE id = $1)", id, as: Bool)
|
||||
video = db.query_one("SELECT * FROM videos WHERE id = $1", id, as: Video)
|
||||
|
||||
# If record was last updated over an hour ago, refresh (expire param in response lasts for 6 hours)
|
||||
if refresh && Time.now - video.updated > 1.hour
|
||||
# If record was last updated over 10 minutes ago, refresh (expire param in response lasts for 6 hours)
|
||||
if refresh && Time.now - video.updated > 10.minutes
|
||||
begin
|
||||
video = fetch_video(id)
|
||||
video_array = video.to_a
|
||||
|
||||
args = arg_array(video_array[1..-1], 2)
|
||||
|
||||
db.exec("UPDATE videos SET (info,updated,title,views,likes,dislikes,wilson_score,\
|
||||
published,description,language,author,ucid, allowed_regions, is_family_friendly, genre)\
|
||||
published,description,language,author,ucid, allowed_regions, is_family_friendly,\
|
||||
genre, genre_url, license)\
|
||||
= (#{args}) WHERE id = $1", video_array)
|
||||
rescue ex
|
||||
db.exec("DELETE FROM videos * WHERE id = $1", id)
|
||||
@ -384,6 +463,7 @@ def get_video(id, db, refresh = true)
|
||||
else
|
||||
video = fetch_video(id)
|
||||
video_array = video.to_a
|
||||
|
||||
args = arg_array(video_array)
|
||||
|
||||
db.exec("INSERT INTO videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", video_array)
|
||||
@ -490,10 +570,19 @@ def fetch_video(id)
|
||||
|
||||
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
|
||||
is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
|
||||
|
||||
genre = html.xpath_node(%q(//meta[@itemprop="genre"])).not_nil!["content"]
|
||||
genre_url = html.xpath_node(%(//a[text()="#{genre}"])).not_nil!["href"]
|
||||
|
||||
license = html.xpath_node(%q(//h4[contains(text(),"License")]/parent::*/ul/li))
|
||||
if license
|
||||
license = license.content
|
||||
else
|
||||
license ||= ""
|
||||
end
|
||||
|
||||
video = Video.new(id, info, Time.now, title, views, likes, dislikes, wilson_score, published, description,
|
||||
nil, author, ucid, allowed_regions, is_family_friendly, genre)
|
||||
nil, author, ucid, allowed_regions, is_family_friendly, genre, genre_url, license)
|
||||
|
||||
return video
|
||||
end
|
||||
@ -504,24 +593,37 @@ end
|
||||
|
||||
def process_video_params(query, preferences)
|
||||
autoplay = query["autoplay"]?.try &.to_i?
|
||||
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
||||
quality = query["quality"]?
|
||||
speed = query["speed"]?.try &.to_f?
|
||||
video_loop = query["loop"]?.try &.to_i?
|
||||
volume = query["volume"]?.try &.to_i?
|
||||
|
||||
if preferences
|
||||
autoplay ||= preferences.autoplay.to_unsafe
|
||||
preferred_captions ||= preferences.captions
|
||||
quality ||= preferences.quality
|
||||
speed ||= preferences.speed
|
||||
video_loop ||= preferences.video_loop.to_unsafe
|
||||
volume ||= preferences.volume
|
||||
end
|
||||
autoplay ||= 0
|
||||
autoplay = autoplay == 1
|
||||
|
||||
autoplay ||= 0
|
||||
preferred_captions ||= [] of String
|
||||
quality ||= "hd720"
|
||||
speed ||= 1
|
||||
video_loop ||= 0
|
||||
volume ||= 100
|
||||
|
||||
autoplay = autoplay == 1
|
||||
video_loop = video_loop == 1
|
||||
|
||||
if query["t"]?
|
||||
video_start = decode_time(query["t"])
|
||||
end
|
||||
video_start ||= 0
|
||||
if query["time_continu"]?
|
||||
video_start = decode_time(query["t"])
|
||||
if query["time_continue"]?
|
||||
video_start = decode_time(query["time_continue"])
|
||||
end
|
||||
video_start ||= 0
|
||||
if query["start"]?
|
||||
@ -542,14 +644,25 @@ def process_video_params(query, preferences)
|
||||
raw ||= 0
|
||||
raw = raw == 1
|
||||
|
||||
quality = query["quality"]?
|
||||
quality ||= "hd720"
|
||||
|
||||
controls = query["controls"]?.try &.to_i?
|
||||
controls ||= 1
|
||||
controls = controls == 1
|
||||
|
||||
return autoplay, video_loop, video_start, video_end, listen, raw, quality, controls
|
||||
params = {
|
||||
autoplay: autoplay,
|
||||
controls: controls,
|
||||
listen: listen,
|
||||
preferred_captions: preferred_captions,
|
||||
quality: quality,
|
||||
raw: raw,
|
||||
speed: speed,
|
||||
video_end: video_end,
|
||||
video_loop: video_loop,
|
||||
video_start: video_start,
|
||||
volume: volume,
|
||||
}
|
||||
|
||||
return params
|
||||
end
|
||||
|
||||
def generate_thumbnails(json, id)
|
||||
|
@ -51,7 +51,7 @@
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-3-5"></div>
|
||||
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
||||
<% if videos.size == 100 %>
|
||||
<% if videos.size == 60 %>
|
||||
<a href="/channel/<%= ucid %>?page=<%= page + 1 %>">Next page</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
@ -1,19 +1,19 @@
|
||||
<video style="width:100%" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>"
|
||||
id="player" class="video-js"
|
||||
<% if autoplay %>autoplay<% end %>
|
||||
<% if video_loop %>loop<% end %>
|
||||
<% if controls %>controls<% end %>>
|
||||
<% if params[:autoplay] %>autoplay<% end %>
|
||||
<% if params[:video_loop] %>loop<% end %>
|
||||
<% if params[:controls] %>controls<% end %>>
|
||||
<% if hlsvp %>
|
||||
<source src="<%= hlsvp %>" type="application/x-mpegURL">
|
||||
<% else %>
|
||||
<% if listen %>
|
||||
<% if params[:listen] %>
|
||||
<% audio_streams.each_with_index do |fmt, i| %>
|
||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>">
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% fmt_stream.each_with_index do |fmt, i| %>
|
||||
<% if preferences %>
|
||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= preferences.quality == fmt["label"].split(" - ")[0] %>">
|
||||
<% if params[:quality] %>
|
||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= params[:quality] == fmt["label"].split(" - ")[0] %>">
|
||||
<% else %>
|
||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= i == 0 ? true : false %>">
|
||||
<% end %>
|
||||
@ -63,8 +63,8 @@ var shareOptions = {
|
||||
title: "<%= video.title.dump_unquoted %>",
|
||||
description: "<%= description %>",
|
||||
image: "<%= thumbnail %>",
|
||||
embedCode: `<iframe id='ivplayer' type='text/html' width='640' height='360'
|
||||
src='<%= host_url %>/embed/<%= video.id %>?<%= host_params %>' frameborder='0'></iframe>`
|
||||
embedCode: "<iframe id='ivplayer' type='text/html' width='640' height='360' \
|
||||
src='<%= host_url %>/embed/<%= video.id %>?<%= host_params %>' frameborder='0'></iframe>"
|
||||
};
|
||||
|
||||
var player = videojs("player", options, function() {
|
||||
@ -110,7 +110,7 @@ var player = videojs("player", options, function() {
|
||||
|
||||
player.share(shareOptions);
|
||||
|
||||
<% if video_start > 0 || video_end > 0 %>
|
||||
<% if params[:video_start] > 0 || params[:video_end] > 0 %>
|
||||
player.markers({
|
||||
onMarkerReached: function(marker) {
|
||||
if (marker.text === "End") {
|
||||
@ -122,19 +122,19 @@ player.markers({
|
||||
}
|
||||
},
|
||||
markers: [
|
||||
{ time: <%= video_start %>, text: "Start" },
|
||||
<% if video_end < 0 %>
|
||||
{ time: <%= params[:video_start] %>, text: "Start" },
|
||||
<% if params[:video_end] < 0 %>
|
||||
{ time: <%= video.info["length_seconds"].to_f - 0.5 %>, text: "End" }
|
||||
<% else %>
|
||||
{ time: <%= video_end %>, text: "End" }
|
||||
{ time: <%= params[:video_end] %>, text: "End" }
|
||||
<% end %>
|
||||
]
|
||||
});
|
||||
|
||||
player.currentTime(<%= video_start %>);
|
||||
player.currentTime(<%= params[:video_start] %>);
|
||||
<% end %>
|
||||
|
||||
<% if !listen %>
|
||||
<% if !params[:listen] %>
|
||||
var currentSources = player.currentSources();
|
||||
for (var i = 0; i < currentSources.length; i++) {
|
||||
if (player.canPlayType(currentSources[i]["type"].split(";")[0]) === "") {
|
||||
@ -146,8 +146,6 @@ for (var i = 0; i < currentSources.length; i++) {
|
||||
player.src(currentSources);
|
||||
<% end %>
|
||||
|
||||
<% if preferences %>
|
||||
player.volume(<%= preferences.volume.to_f / 100 %>);
|
||||
player.playbackRate(<%= preferences.speed %>);
|
||||
<% end %>
|
||||
player.volume(<%= params[:volume].to_f / 100 %>);
|
||||
player.playbackRate(<%= params[:speed] %>);
|
||||
</script>
|
||||
|
@ -1,12 +1,11 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/video.js@6.12.0/dist/video-js.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/silvermine-videojs-quality-selector@1.1.2/dist/css/quality-selector.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/videojs-markers@1.0.1/dist/videojs.markers.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/videojs-share@1.1.0/dist/videojs-share.css">
|
||||
<script src="https://unpkg.com/video.js@6.12.0/dist/video.min.js"></script>
|
||||
<script src="https://unpkg.com/videojs-hotkeys@0.2.22/build/videojs.hotkeys.min.js"></script>
|
||||
<script src="https://unpkg.com/silvermine-videojs-quality-selector@1.1.2/dist/js/silvermine-videojs-quality-selector.min.js"></script>
|
||||
<script src="https://unpkg.com/videojs-markers@1.0.1/dist/videojs-markers.min.js"></script>
|
||||
<script src="https://unpkg.com/videojs-share@1.1.0/dist/videojs-share.min.js"></script>
|
||||
<% if hlsvp %>
|
||||
<script src="https://unpkg.com/@videojs/http-streaming@1.2.2/dist/videojs-http-streaming.min.js"></script>
|
||||
<% end %>
|
||||
<link rel="stylesheet" href="/css/video-js.min.css">
|
||||
<link rel="stylesheet" href="/css/quality-selector.css">
|
||||
<link rel="stylesheet" href="/css/videojs.markers.min.css">
|
||||
<link rel="stylesheet" href="/css/videojs-share.css">
|
||||
<script src="/js/video.min.js"></script>
|
||||
<script src="/js/videojs.hotkeys.min.js"></script>
|
||||
<script src="/js/silvermine-videojs-quality-selector.min.js"></script>
|
||||
<script src="/js/videojs-markers.min.js"></script>
|
||||
<script src="/js/videojs-share.min.js"></script>
|
||||
<script src="/js/videojs-http-streaming.min.js"></script>
|
||||
<script src="/js/videojs-contrib-quality-levels.min.js"></script>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="pure-u-1 pure-u-md-1-4">
|
||||
<div class="h-box">
|
||||
<% if video.responds_to?(:playlists) %>
|
||||
<% if video.responds_to?(:playlists) && !video.playlists.empty? %>
|
||||
<% params = "&list=#{video.playlists[0]}" %>
|
||||
<% else %>
|
||||
<% params = nil %>
|
||||
@ -8,7 +8,7 @@
|
||||
<a style="width:100%;" href="/watch?v=<%= video.id %><%= params %>">
|
||||
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
|
||||
<% else %>
|
||||
<img style="width:100%;" src="https://i.ytimg.com/vi/<%= video.id %>/mqdefault.jpg"/>
|
||||
<img style="width:100%;" src="/vi/<%= video.id %>/mqdefault.jpg"/>
|
||||
<% end %>
|
||||
<p><%= video.title %></p>
|
||||
</a>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control" method="post">
|
||||
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= referer %>" method="post">
|
||||
<fieldset>
|
||||
<legend>Import</legend>
|
||||
|
||||
|
@ -6,6 +6,11 @@
|
||||
<div class="pure-u-2-3">
|
||||
<h3><%= playlist.title %></h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3" style="text-align:right;">
|
||||
<h3>
|
||||
<a href="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1 pure-u-md-1-4">
|
||||
@ -16,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<p><%= playlist.description %></p>
|
||||
<p><%= playlist.description_html %></p>
|
||||
</div>
|
||||
|
||||
<% videos.each_slice(4) do |slice| %>
|
||||
|
@ -48,10 +48,19 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="comments">Pull comments from: </label>
|
||||
<select name="comments" id="comments">
|
||||
<% {"youtube", "reddit"}.each do |option| %>
|
||||
<option <% if user.preferences.comments == option %> selected <% end %>><%= option %></option>
|
||||
<label for="comments_0">Default comments: </label>
|
||||
<select name="comments_0" id="comments_0">
|
||||
<% {"", "youtube", "reddit"}.each do |option| %>
|
||||
<option <% if user.preferences.comments[0] == option %> selected <% end %>><%= option %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="comments_1">Fallback comments: </label>
|
||||
<select name="comments_1" id="comments_1">
|
||||
<% {"", "youtube", "reddit"}.each do |option| %>
|
||||
<option <% if user.preferences.comments[1] == option %> selected <% end %>><%= option %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
@ -66,7 +75,7 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="captions_fallback">Fallback languages: </label>
|
||||
<label for="captions_fallback">Fallback captions: </label>
|
||||
<select class="pure-u-1-5" name="captions_1" id="captions_1">
|
||||
<% CAPTION_LANGUAGES.each do |option| %>
|
||||
<option <% if user.preferences.captions[1] == option %> selected <% end %>><%= option %></option>
|
||||
@ -80,7 +89,13 @@ function update_value(element) {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="related_videos">Show related videos? </label>
|
||||
<input name="related_videos" id="related_videos" type="checkbox" <% if user.preferences.related_videos %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<legend>Visual preferences</legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="dark_mode">Dark mode: </label>
|
||||
<input name="dark_mode" id="dark_mode" type="checkbox" <% if user.preferences.dark_mode %>checked<% end %>>
|
||||
@ -92,6 +107,7 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<legend>Subscription preferences</legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="redirect_feed">Redirect homepage to feed: </label>
|
||||
<input name="redirect_feed" id="redirect_feed" type="checkbox" <% if user.preferences.redirect_feed %>checked<% end %>>
|
||||
@ -127,12 +143,13 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<legend>Data preferences</legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/clear_watch_history">Clear watch history</a>
|
||||
<a href="/clear_watch_history?referer=<%= referer %>">Clear watch history</a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/data_control">Import/Export data</a>
|
||||
<a href="/data_control?referer=<%= referer %>">Import/Export data</a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= query.not_nil!.size > 30 ? query.not_nil![0,30].rstrip(".") + "..." : query.not_nil! %> - Invidious</title>
|
||||
<title><%= search_query.not_nil!.size > 30 ? query.not_nil![0,30].rstrip(".") + "..." : query.not_nil! %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<% videos.each_slice(4) do |slice| %>
|
||||
@ -18,6 +18,8 @@
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-3-5"></div>
|
||||
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
||||
<% if count >= 20 %>
|
||||
<a href="/search?q=<%= query %>&page=<%= page + 1 %>">Next page</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="pure-u-1-3" style="text-align:right;">
|
||||
<h3>
|
||||
<a href="/data_control">Import/Export</a>
|
||||
<a href="/data_control?referer=<%= referer %>">Import/Export</a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,6 +16,13 @@
|
||||
</div>
|
||||
|
||||
<center><%= notifications.size %> unseen notifications</center>
|
||||
|
||||
<% if !notifications.empty? %>
|
||||
<div class="h-box">
|
||||
<hr>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% notifications.each_slice(4) do |slice| %>
|
||||
<div class="pure-g">
|
||||
<% slice.each do |video| %>
|
||||
|
@ -4,10 +4,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<%= yield_content "header" %>
|
||||
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/ionicons@4.2.6/dist/css/ionicons.min.css">
|
||||
<link rel="stylesheet" href="/css/pure-min.css">
|
||||
<link rel="stylesheet" href="/css/grids-responsive-min.css">
|
||||
<link rel="stylesheet" href="/css/ionicons.min.css">
|
||||
<link rel="stylesheet" href="/css/default.css">
|
||||
<% if env.get?("user") && env.get("user").as(User).preferences.dark_mode %>
|
||||
<link rel="stylesheet" href="/css/darktheme.css">
|
||||
@ -18,8 +19,8 @@
|
||||
|
||||
<body>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-4-24"></div>
|
||||
<div class="pure-u-1 pure-u-md-16-24">
|
||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||
<div class="pure-u-1 pure-u-md-20-24">
|
||||
<div class="pure-g navbar h-box">
|
||||
<div class="pure-u-1 pure-u-md-4-24">
|
||||
<a href="/" class="index-link pure-menu-heading">Invidious</a>
|
||||
@ -27,14 +28,14 @@
|
||||
<div class="pure-u-1 pure-u-md-12-24 searchbar">
|
||||
<form class="pure-form" action="/search" method="get">
|
||||
<fieldset>
|
||||
<input type="search" style="width:100%;" name="q" placeholder="search" value="<%= env.params.query["q"]? %>">
|
||||
<input type="search" style="width:100%;" name="q" placeholder="search" value="<%= env.params.query["q"]? || env.get? "search" %>">
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
||||
<% if env.get? "user" %>
|
||||
<div class="pure-u-1-4">
|
||||
<a href="/toggle_theme?referer=<%= env.get("current_page") %>" class="pure-menu-heading">
|
||||
<a href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<% preferences = env.get("user").as(User).preferences %>
|
||||
<% if preferences.dark_mode %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
@ -54,15 +55,15 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a href="/preferences?referer=<%= env.get("current_page") %>" class="pure-menu-heading">
|
||||
<a href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<i class="icon ion-ios-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a href="/signout?referer=<%= env.get("current_page") %>" class="pure-menu-heading">Sign out</a>
|
||||
<a href="/signout?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">Sign out</a>
|
||||
</div>
|
||||
<% else %>
|
||||
<a href="/login?referer=<%= env.get("current_page") %>" class="pure-menu-heading">Login</a>
|
||||
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">Login</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@ -72,6 +73,11 @@
|
||||
Roth</a>.
|
||||
Source available <a
|
||||
href="https://github.com/omarroth/invidious">here</a>.
|
||||
<p>Liberapay:
|
||||
<a href="https://liberapay.com/omarroth">
|
||||
https://liberapay.com/omarroth
|
||||
</a>
|
||||
</p>
|
||||
<p>Patreon:
|
||||
<a href="https://patreon.com/omarroth">
|
||||
https://patreon.com/omarroth
|
||||
@ -81,8 +87,7 @@
|
||||
<p>BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-4-24"></div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<meta property="og:site_name" content="Invidious">
|
||||
<meta property="og:url" content="<%= host_url %>/watch?v=<%= video.id %>">
|
||||
<meta property="og:title" content="<%= HTML.escape(video.title) %>">
|
||||
<meta property="og:image" content="https://i.ytimg.com/vi/<%= video.id %>/hqdefault.jpg">
|
||||
<meta property="og:image" content="/vi/<%= video.id %>/maxres.jpg">
|
||||
<meta property="og:description" content="<%= description %>">
|
||||
<meta property="og:type" content="video.other">
|
||||
<meta property="og:video:url" content="<%= host_url %>/embed/<%= video.id %>">
|
||||
@ -26,167 +26,14 @@
|
||||
<title><%= HTML.escape(video.title) %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<div id="player-container" class="h-box">
|
||||
<%= rendered "components/player" %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggle(target) {
|
||||
body = target.parentNode.parentNode.children[1];
|
||||
if (body.style.display === null || body.style.display === "") {
|
||||
target.innerHTML = "[ + ]";
|
||||
body.style.display = "none";
|
||||
} else {
|
||||
target.innerHTML = "[ - ]";
|
||||
body.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function toggle_comments(target) {
|
||||
body = target.parentNode.parentNode.parentNode.children[1];
|
||||
if (body.style.display === null || body.style.display === "") {
|
||||
target.innerHTML = "[ + ]";
|
||||
body.style.display = "none";
|
||||
} else {
|
||||
target.innerHTML = "[ - ]";
|
||||
body.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function load_comments(target) {
|
||||
var continuation = target.getAttribute("data-continuation");
|
||||
|
||||
var body = target.parentNode.parentNode;
|
||||
var fallback = body.innerHTML;
|
||||
body.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
|
||||
var url =
|
||||
"/api/v1/comments/<%= video.id %>?format=html&continuation=" + continuation;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
body.innerHTML = xhr.response.contentHtml;
|
||||
} else {
|
||||
body.innerHTML = fallback;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
body.innerHTML = fallback;
|
||||
};
|
||||
}
|
||||
|
||||
function get_reddit_comments() {
|
||||
var url = "/api/v1/comments/<%= video.id %>?source=reddit";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4)
|
||||
if (xhr.status == 200) {
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML = `
|
||||
<div>
|
||||
<h3>
|
||||
<a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a>
|
||||
{title}
|
||||
</h3>
|
||||
<b>
|
||||
<a target="_blank" href="https://reddit.com{permalink}">View more comments on Reddit</a>
|
||||
</b>
|
||||
</div>
|
||||
<div>{contentHtml}</div>
|
||||
|
||||
<hr>`.supplant({
|
||||
title: xhr.response.title,
|
||||
permalink: xhr.response.permalink,
|
||||
contentHtml: xhr.response.contentHtml
|
||||
});
|
||||
} else {
|
||||
get_youtube_comments();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
get_reddit_comments();
|
||||
};
|
||||
}
|
||||
|
||||
function get_youtube_comments() {
|
||||
var url = "/api/v1/comments/<%= video.id %>?format=html";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4)
|
||||
if (xhr.status == 200) {
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML = `
|
||||
<div>
|
||||
<h3>
|
||||
<a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a>
|
||||
View {commentCount} comments
|
||||
</h3>
|
||||
</div>
|
||||
<div>{contentHtml}</div>
|
||||
<hr>`.supplant({
|
||||
contentHtml: xhr.response.contentHtml,
|
||||
commentCount: commaSeparateNumber(xhr.response.commentCount)
|
||||
});
|
||||
} else {
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML = "";
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
get_youtube_comments();
|
||||
};
|
||||
}
|
||||
|
||||
function commaSeparateNumber(val){
|
||||
while (/(\d+)(\d{3})/.test(val.toString())){
|
||||
val = val.toString().replace(/(\d+)(\d{3})/, '$1'+','+'$2');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
String.prototype.supplant = function(o) {
|
||||
return this.replace(/{([^{}]*)}/g, function(a, b) {
|
||||
var r = o[b];
|
||||
return typeof r === "string" || typeof r === "number" ? r : a;
|
||||
});
|
||||
};
|
||||
|
||||
<% if preferences && preferences.comments == "reddit" %>
|
||||
get_reddit_comments();
|
||||
<% else %>
|
||||
get_youtube_comments();
|
||||
<% end %>
|
||||
|
||||
</script>
|
||||
|
||||
<div class="h-box">
|
||||
<h1>
|
||||
<%= HTML.escape(video.title) %>
|
||||
<% if listen %>
|
||||
<% if params[:listen] %>
|
||||
<a href="/watch?<%= env.params.query %>">
|
||||
<i class="icon ion-ios-videocam"></i>
|
||||
</a>
|
||||
@ -208,7 +55,10 @@ get_youtube_comments();
|
||||
<p><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
|
||||
<p><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
|
||||
<p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
|
||||
<p id="Genre">Genre: <%= video.genre %></p>
|
||||
<p id="Genre">Genre: <a href="<%= video.genre_url %>"><%= video.genre %></a></p>
|
||||
<% if !video.license.empty? %>
|
||||
<p id="License">License: <%= video.license %></p>
|
||||
<% end %>
|
||||
<p id="FamilyFriendly">Family Friendly? <%= video.is_family_friendly %></p>
|
||||
<p id="Wilson">Wilson Score: <%= video.wilson_score.round(4) %></p>
|
||||
<p id="Rating">Rating: <%= rating.round(4) %> / 5</p>
|
||||
@ -266,13 +116,14 @@ get_youtube_comments();
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
<% if !preferences || preferences && preferences.related_videos %>
|
||||
<div class="h-box">
|
||||
<% rvs.each do |rv| %>
|
||||
<% if rv.has_key?("id") %>
|
||||
<a href="/watch?v=<%= rv["id"] %>">
|
||||
<% if preferences && preferences.thin_mode %>
|
||||
<% else %>
|
||||
<img style="width:100%;" src="<%= rv["iurlmq"] %>">
|
||||
<img style="width:100%;" src="/vi/<%= rv["id"] %>/mqdefault.jpg">
|
||||
<% end %>
|
||||
<p style="width:100%"><%= rv["title"] %></p>
|
||||
<p>
|
||||
@ -282,5 +133,190 @@ get_youtube_comments();
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggle(target) {
|
||||
body = target.parentNode.parentNode.children[1];
|
||||
if (body.style.display === null || body.style.display === "") {
|
||||
target.innerHTML = "[ + ]";
|
||||
body.style.display = "none";
|
||||
} else {
|
||||
target.innerHTML = "[ - ]";
|
||||
body.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function toggle_comments(target) {
|
||||
body = target.parentNode.parentNode.parentNode.children[1];
|
||||
if (body.style.display === null || body.style.display === "") {
|
||||
target.innerHTML = "[ + ]";
|
||||
body.style.display = "none";
|
||||
} else {
|
||||
target.innerHTML = "[ - ]";
|
||||
body.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function get_youtube_replies(target) {
|
||||
var continuation = target.getAttribute("data-continuation");
|
||||
|
||||
var body = target.parentNode.parentNode;
|
||||
var fallback = body.innerHTML;
|
||||
body.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
|
||||
var url =
|
||||
"/api/v1/comments/<%= video.id %>?format=html&continuation=" + continuation;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
body.innerHTML = xhr.response.contentHtml;
|
||||
} else {
|
||||
body.innerHTML = fallback;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
console.log("Pulling comments timed out.");
|
||||
|
||||
body.innerHTML = fallback;
|
||||
};
|
||||
}
|
||||
|
||||
function get_reddit_comments() {
|
||||
var url = "/api/v1/comments/<%= video.id %>?source=reddit&format=html";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4)
|
||||
if (xhr.status == 200) {
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML = ' \
|
||||
<div> \
|
||||
<h3> \
|
||||
<a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a> \
|
||||
{title} \
|
||||
</h3> \
|
||||
<b> \
|
||||
<a target="_blank" href="https://reddit.com{permalink}">View more comments on Reddit</a> \
|
||||
</b> \
|
||||
</div> \
|
||||
<div>{contentHtml}</div> \
|
||||
<hr>'.supplant({
|
||||
title: xhr.response.title,
|
||||
permalink: xhr.response.permalink,
|
||||
contentHtml: xhr.response.contentHtml
|
||||
});
|
||||
} else {
|
||||
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||
get_youtube_comments();
|
||||
<% else %>
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML = "";
|
||||
<% end %>
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
console.log("Pulling comments timed out.");
|
||||
|
||||
get_reddit_comments();
|
||||
};
|
||||
}
|
||||
|
||||
function get_youtube_comments() {
|
||||
var url = "/api/v1/comments/<%= video.id %>?format=html";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4)
|
||||
if (xhr.status == 200) {
|
||||
comments = document.getElementById("comments");
|
||||
if (xhr.response.commentCount > 0) {
|
||||
comments.innerHTML = ' \
|
||||
<div> \
|
||||
<h3> \
|
||||
<a href="javascript:void(0)" onclick="toggle_comments(this)">[ - ]</a> \
|
||||
View {commentCount} comments \
|
||||
</h3> \
|
||||
</div> \
|
||||
<div>{contentHtml}</div> \
|
||||
<hr>'.supplant({
|
||||
contentHtml: xhr.response.contentHtml,
|
||||
commentCount: commaSeparateNumber(xhr.response.commentCount)
|
||||
});
|
||||
} else {
|
||||
comments.innerHTML = "";
|
||||
}
|
||||
} else {
|
||||
<% if preferences && preferences.comments[1] == "youtube" %>
|
||||
get_youtube_comments();
|
||||
<% else %>
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML = "";
|
||||
<% end %>
|
||||
}
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
console.log("Pulling comments timed out.");
|
||||
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
get_youtube_comments();
|
||||
};
|
||||
}
|
||||
|
||||
function commaSeparateNumber(val){
|
||||
while (/(\d+)(\d{3})/.test(val.toString())){
|
||||
val = val.toString().replace(/(\d+)(\d{3})/, '$1'+','+'$2');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
String.prototype.supplant = function(o) {
|
||||
return this.replace(/{([^{}]*)}/g, function(a, b) {
|
||||
var r = o[b];
|
||||
return typeof r === "string" || typeof r === "number" ? r : a;
|
||||
});
|
||||
};
|
||||
|
||||
<% if preferences %>
|
||||
<% if preferences.comments[0] == "youtube" %>
|
||||
get_youtube_comments();
|
||||
<% elsif preferences.comments[0] == "reddit" %>
|
||||
get_reddit_comments();
|
||||
<% else %>
|
||||
<% if preferences.comments[1] == "youtube" %>
|
||||
get_youtube_comments();
|
||||
<% elsif preferences.comments[1] == "reddit" %>
|
||||
get_reddit_comments();
|
||||
<% else %>
|
||||
comments = document.getElementById("comments");
|
||||
comments.innerHTML = "";
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
get_youtube_comments();
|
||||
<% end %>
|
||||
|
||||
</script>
|
Reference in New Issue
Block a user