diff options
| -rw-r--r-- | assets/css/videojs-vtt-thumbnails-fix.css | 3 | ||||
| -rw-r--r-- | assets/js/player.js | 3 | ||||
| -rw-r--r-- | assets/js/videojs-vtt-thumbnails.min.js | 4 | ||||
| -rw-r--r-- | src/invidious.cr | 201 | ||||
| -rw-r--r-- | src/invidious/channels.cr | 4 | ||||
| -rw-r--r-- | src/invidious/routes/embed/index.cr | 27 | ||||
| -rw-r--r-- | src/invidious/routes/embed/show.cr | 174 | ||||
| -rw-r--r-- | src/invidious/views/components/player_sources.ecr | 1 | ||||
| -rw-r--r-- | src/invidious/views/search.ecr | 18 | ||||
| -rw-r--r-- | src/invidious/views/template.ecr | 1 |
10 files changed, 231 insertions, 205 deletions
diff --git a/assets/css/videojs-vtt-thumbnails-fix.css b/assets/css/videojs-vtt-thumbnails-fix.css new file mode 100644 index 00000000..8b62cf0c --- /dev/null +++ b/assets/css/videojs-vtt-thumbnails-fix.css @@ -0,0 +1,3 @@ +.video-js .vjs-vtt-thumbnail-display { + max-width: 158px; +} diff --git a/assets/js/player.js b/assets/js/player.js index edab35bf..f79fbbf3 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -149,7 +149,8 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') { } player.vttThumbnails({ - src: location.origin + '/api/v1/storyboards/' + video_data.id + '?height=90' + src: location.origin + '/api/v1/storyboards/' + video_data.id + '?height=90', + showTimestamp: true }); // Enable annotations diff --git a/assets/js/videojs-vtt-thumbnails.min.js b/assets/js/videojs-vtt-thumbnails.min.js index e1efca62..be86a201 100644 --- a/assets/js/videojs-vtt-thumbnails.min.js +++ b/assets/js/videojs-vtt-thumbnails.min.js @@ -1,7 +1,7 @@ /** * videojs-vtt-thumbnails * @version 0.0.13 - * @copyright 2019 Chris Boustead <chris@forgemotion.com> + * @copyright 2020 Chris Boustead <chris@forgemotion.com> * @license MIT */ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],e):t.videojsVttThumbnails=e(t.videojs)}(this,function(i){"use strict";i=i&&i.hasOwnProperty("default")?i.default:i;!function(){function l(t){this.value=t}function t(i){var o,n;function a(t,e){try{var r=i[t](e),s=r.value;s instanceof l?Promise.resolve(s.value).then(function(t){a("next",t)},function(t){a("throw",t)}):u(r.done?"return":"normal",r.value)}catch(t){u("throw",t)}}function u(t,e){switch(t){case"return":o.resolve({value:e,done:!0});break;case"throw":o.reject(e);break;default:o.resolve({value:e,done:!1})}(o=o.next)?a(o.key,o.arg):n=null}this._invoke=function(s,i){return new Promise(function(t,e){var r={key:s,arg:i,resolve:t,reject:e,next:null};n?n=n.next=r:(o=n=r,a(s,i))})},"function"!=typeof i.return&&(this.return=void 0)}"function"==typeof Symbol&&Symbol.asyncIterator&&(t.prototype[Symbol.asyncIterator]=function(){return this}),t.prototype.next=function(t){return this._invoke("next",t)},t.prototype.throw=function(t){return this._invoke("throw",t)},t.prototype.return=function(t){return this._invoke("return",t)}}();var o={},n={},t=i.registerPlugin||i.plugin,e=function(r){var s=this;this.ready(function(){var t,e;t=s,e=i.mergeOptions(o,r),t.addClass("vjs-vtt-thumbnails"),t.vttThumbnails=new a(t,e)})},a=function(){function r(t,e){return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,r),this.player=t,this.options=e,this.listenForDurationChange(),this.initializeThumbnails(),this.registeredEvents={},this}return r.prototype.src=function(t){this.resetPlugin(),this.options.src=t,this.initializeThumbnails()},r.prototype.detach=function(){this.resetPlugin()},r.prototype.resetPlugin=function(){this.thumbnailHolder&&this.thumbnailHolder.parentNode.removeChild(this.thumbnailHolder),this.progressBar&&this.progressBar.removeEventListener("mouseenter",this.registeredEvents.progressBarMouseEnter),this.progressBar&&this.progressBar.removeEventListener("mouseleave",this.registeredEvents.progressBarMouseLeave),this.progressBar&&this.progressBar.removeEventListener("mousemove",this.registeredEvents.progressBarMouseMove),delete this.registeredEvents.progressBarMouseEnter,delete this.registeredEvents.progressBarMouseLeave,delete this.registeredEvents.progressBarMouseMove,delete this.progressBar,delete this.vttData,delete this.thumbnailHolder,delete this.lastStyle},r.prototype.listenForDurationChange=function(){this.player.on("durationchange",function(){})},r.prototype.initializeThumbnails=function(){var e=this;if(this.options.src){var t=this.getBaseUrl(),r=this.getFullyQualifiedUrl(this.options.src,t);this.getVttFile(r).then(function(t){e.vttData=e.processVtt(t),e.setupThumbnailElement()})}},r.prototype.getBaseUrl=function(){return[window.location.protocol,"//",window.location.hostname,window.location.port?":"+window.location.port:"",window.location.pathname].join("").split(/([^\/]*)$/gi).shift()},r.prototype.getVttFile=function(s){var i=this;return new Promise(function(t,e){var r=new XMLHttpRequest;r.data={resolve:t},r.addEventListener("load",i.vttFileLoaded),r.open("GET",s),r.send()})},r.prototype.vttFileLoaded=function(){this.data.resolve(this.responseText)},r.prototype.setupThumbnailElement=function(t){var e=this;this.progressBar=this.player.$(".vjs-progress-control");var r=document.createElement("div");r.setAttribute("class","vjs-vtt-thumbnail-display"),this.progressBar.appendChild(r),this.thumbnailHolder=r,this.registeredEvents.progressBarMouseEnter=function(){return e.onBarMouseenter()},this.registeredEvents.progressBarMouseLeave=function(){return e.onBarMouseleave()},this.progressBar.addEventListener("mouseenter",this.registeredEvents.progressBarMouseEnter),this.progressBar.addEventListener("mouseleave",this.registeredEvents.progressBarMouseLeave)},r.prototype.onBarMouseenter=function(){var e=this;this.mouseMoveCallback=function(t){e.onBarMousemove(t)},this.registeredEvents.progressBarMouseMove=this.mouseMoveCallback,this.progressBar.addEventListener("mousemove",this.registeredEvents.progressBarMouseMove),this.showThumbnailHolder()},r.prototype.onBarMouseleave=function(){this.registeredEvents.progressBarMouseMove&&this.progressBar.removeEventListener("mousemove",this.registeredEvents.progressBarMouseMove),this.hideThumbnailHolder()},r.prototype.getXCoord=function(t,e){var r=t.getBoundingClientRect(),s=document.documentElement;return e-(r.left+(window.pageXOffset||s.scrollLeft||0))},r.prototype.onBarMousemove=function(t){this.updateThumbnailStyle(this.getXCoord(this.progressBar,t.clientX),this.progressBar.offsetWidth)},r.prototype.getStyleForTime=function(t){for(var e=0;e<this.vttData.length;++e){var r=this.vttData[e];if(t>=r.start&&t<r.end){if(r.css.url&&!n[r.css.url]){var s=new Image;s.src=r.css.url,n[r.css.url]=s}return r.css}}},r.prototype.showThumbnailHolder=function(){this.thumbnailHolder.style.opacity="1"},r.prototype.hideThumbnailHolder=function(){this.thumbnailHolder.style.opacity="0"},r.prototype.updateThumbnailStyle=function(t,e){var r=(1-(e-t)/e)*this.player.duration(),s=this.getStyleForTime(r);if(!s)return this.hideThumbnailHolder();var i=(1-(e-t)/e)*e;if(this.thumbnailHolder.style.transform="translateX("+i+"px)",this.thumbnailHolder.style.marginLeft="-"+parseInt(s.width)/2+"px",!this.lastStyle||this.lastStyle!==s)for(var o in this.lastStyle=s)s.hasOwnProperty(o)&&(this.thumbnailHolder.style[o]=s[o])},r.prototype.processVtt=function(t){var a=this,u=[];return t.split(/[\r\n][\r\n]/i).forEach(function(t){if(t.match(/([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?( ?--> ?)([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?[\r\n]{1}.*/gi)){var e=t.split(/[\r\n]/i),r=e[0].split(/ ?--> ?/i),s=r[0],i=r[1],o=e[1],n=a.getVttCss(o);u.push({start:a.getSecondsFromTimestamp(s),end:a.getSecondsFromTimestamp(i),css:n})}}),u},r.prototype.getFullyQualifiedUrl=function(t,e){return 0<=t.indexOf("//")?t:0===e.indexOf("//")?[e.replace(/\/$/gi,""),this.trim(t,"/")].join("/"):0<e.indexOf("//")?[this.trim(e,"/"),this.trim(t,"/")].join("/"):t},r.prototype.getPropsFromDef=function(t){var e=t.split(/#xywh=/i),r=e[0],s=e[1].match(/[0-9]+/gi);return{x:s[0],y:s[1],w:s[2],h:s[3],image:r}},r.prototype.getVttCss=function(t){var e={},r=void 0;if(r=0<=this.options.src.indexOf("//")?this.options.src.split(/([^\/]*)$/gi).shift():this.getBaseUrl()+this.options.src.split(/([^\/]*)$/gi).shift(),!(t=this.getFullyQualifiedUrl(t,r)).match(/#xywh=/i))return e.background='url("'+t+'")',e;var s=this.getPropsFromDef(t);return e.background='url("'+s.image+'") no-repeat -'+s.x+"px -"+s.y+"px",e.width=s.w+"px",e.height=s.h+"px",e.url=s.image,e},r.prototype.doconstructTimestamp=function(t){var e=t.split("."),r=e[0].split(":");return{milliseconds:parseInt(e[1])||0,seconds:parseInt(r.pop())||0,minutes:parseInt(r.pop())||0,hours:parseInt(r.pop())||0}},r.prototype.getSecondsFromTimestamp=function(t){var e=this.doconstructTimestamp(t);return parseInt(3600*e.hours+60*e.minutes+e.seconds+e.milliseconds/1e3)},r.prototype.trim=function(t,e){var r=[" ","\n","\r","\t","\f","\v"," "," "," "," "," "," "," "," "," "," "," "," ","","\u2028","\u2029"," "].join(""),s=0,i=0;for(t+="",e&&(r=(e+"").replace(/([[\]().?/*{}+$^:])/g,"$1")),s=t.length,i=0;i<s;i++)if(-1===r.indexOf(t.charAt(i))){t=t.substring(i);break}for(i=(s=t.length)-1;0<=i;i--)if(-1===r.indexOf(t.charAt(i))){t=t.substring(0,i+1);break}return-1===r.indexOf(t.charAt(0))?t:""},r}();return t("vttThumbnails",e),e.VERSION="0.0.13",e});
\ No newline at end of file +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],t):(e=e||self).videojsVttThumbnails=t(e.videojs)}(this,function(i){"use strict";i=i&&Object.prototype.hasOwnProperty.call(i,"default")?i.default:i;function e(s){var r=this;this.ready(function(){var e,t;e=r,t=i.mergeOptions(n,s),e.addClass("vjs-vtt-thumbnails"),e.vttThumbnails=new a(e,t)})}var n={},o={},t=i.registerPlugin||i.plugin,a=function(){function e(e,t){return this.player=e,this.options=t,this.listenForDurationChange(),this.initializeThumbnails(),this.registeredEvents={},this}var t=e.prototype;return t.src=function(e){this.resetPlugin(),this.options.src=e,this.initializeThumbnails()},t.detach=function(){this.resetPlugin()},t.resetPlugin=function(){this.thumbnailHolder&&this.thumbnailHolder.parentNode.removeChild(this.thumbnailHolder),this.progressBar&&(this.progressBar.removeEventListener("mouseenter",this.registeredEvents.progressBarMouseEnter),this.progressBar.removeEventListener("mouseleave",this.registeredEvents.progressBarMouseLeave),this.progressBar.removeEventListener("mousemove",this.registeredEvents.progressBarMouseMove)),delete this.registeredEvents.progressBarMouseEnter,delete this.registeredEvents.progressBarMouseLeave,delete this.registeredEvents.progressBarMouseMove,delete this.progressBar,delete this.vttData,delete this.thumbnailHolder,delete this.lastStyle},t.listenForDurationChange=function(){this.player.on("durationchange",function(){})},t.initializeThumbnails=function(){var e,t,s=this;this.options.src&&(e=this.getBaseUrl(),t=this.getFullyQualifiedUrl(this.options.src,e),this.getVttFile(t).then(function(e){s.vttData=s.processVtt(e),s.setupThumbnailElement()}))},t.getBaseUrl=function(){return[window.location.protocol,"//",window.location.hostname,window.location.port?":"+window.location.port:"",window.location.pathname].join("").split(/([^\/]*)$/gi).shift()},t.getVttFile=function(r){var i=this;return new Promise(function(e,t){var s=new XMLHttpRequest;s.data={resolve:e},s.addEventListener("load",i.vttFileLoaded),s.open("GET",r),s.overrideMimeType("text/plain; charset=utf-8"),s.send()})},t.vttFileLoaded=function(){this.data.resolve(this.responseText)},t.setupThumbnailElement=function(){var e=this,t=null;this.options.showTimestamp||(t=this.player.$(".vjs-mouse-display"));var s=document.createElement("div");s.setAttribute("class","vjs-vtt-thumbnail-display"),this.progressBar=this.player.$(".vjs-progress-control"),this.progressBar.appendChild(s),this.thumbnailHolder=s,t&&!this.options.showTimestamp&&t.classList.add("vjs-hidden"),this.registeredEvents.progressBarMouseEnter=function(){return e.onBarMouseenter()},this.registeredEvents.progressBarMouseLeave=function(){return e.onBarMouseleave()},this.progressBar.addEventListener("mouseenter",this.registeredEvents.progressBarMouseEnter),this.progressBar.addEventListener("mouseleave",this.registeredEvents.progressBarMouseLeave)},t.onBarMouseenter=function(){var t=this;this.mouseMoveCallback=function(e){t.onBarMousemove(e)},this.registeredEvents.progressBarMouseMove=this.mouseMoveCallback,this.progressBar.addEventListener("mousemove",this.registeredEvents.progressBarMouseMove),this.showThumbnailHolder()},t.onBarMouseleave=function(){this.registeredEvents.progressBarMouseMove&&this.progressBar.removeEventListener("mousemove",this.registeredEvents.progressBarMouseMove),this.hideThumbnailHolder()},t.getXCoord=function(e,t){var s=e.getBoundingClientRect(),r=document.documentElement;return t-(s.left+(window.pageXOffset||r.scrollLeft||0))},t.onBarMousemove=function(e){this.updateThumbnailStyle(i.dom.getPointerPosition(this.progressBar,e).x,this.progressBar.offsetWidth)},t.getStyleForTime=function(e){for(var t=0;t<this.vttData.length;++t){var s,r=this.vttData[t];if(e>=r.start&&e<r.end)return r.css.url&&!o[r.css.url]&&((s=new Image).src=r.css.url,o[r.css.url]=s),r.css}},t.showThumbnailHolder=function(){this.thumbnailHolder.style.opacity="1"},t.hideThumbnailHolder=function(){this.thumbnailHolder.style.opacity="0"},t.updateThumbnailStyle=function(e,t){var s=e*this.player.duration(),r=this.getStyleForTime(s);if(!r)return this.hideThumbnailHolder();var i=e*t,n=parseInt(r.width,10),o=n>>1,a=t-(i+o),l=i-o;if(0<l&&0<a?this.thumbnailHolder.style.transform="translateX("+(i-o)+"px)":l<=0?this.thumbnailHolder.style.transform="translateX(0px)":a<=0&&(this.thumbnailHolder.style.transform="translateX("+(t-n)+"px)"),!this.lastStyle||this.lastStyle!==r)for(var u in this.lastStyle=r)r.hasOwnProperty(u)&&(this.thumbnailHolder.style[u]=r[u])},t.processVtt=function(e){var a=this,l=[];return e.split(/[\r\n][\r\n]/i).forEach(function(e){var t,s,r,i,n,o;e.match(/([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?( ?--> ?)([0-9]{2}:)?([0-9]{2}:)?[0-9]{2}(.[0-9]{3})?[\r\n]{1}.*/gi)&&(r=(s=(t=e.split(/[\r\n]/i))[0].split(/ ?--> ?/i))[0],i=s[1],n=t[1],o=a.getVttCss(n),l.push({start:a.getSecondsFromTimestamp(r),end:a.getSecondsFromTimestamp(i),css:o}))}),l},t.getFullyQualifiedUrl=function(e,t){return 0<=e.indexOf("//")?e:0===t.indexOf("//")?[t.replace(/\/$/gi,""),this.trim(e,"/")].join("/"):0<t.indexOf("//")?[this.trim(t,"/"),this.trim(e,"/")].join("/"):e},t.getPropsFromDef=function(e){var t=e.split(/#xywh=/i),s=t[0],r=t[1].match(/[0-9]+/gi);return{x:r[0],y:r[1],w:r[2],h:r[3],image:s}},t.getVttCss=function(e){var t={},s=0<=this.options.src.indexOf("//")?this.options.src.split(/([^\/]*)$/gi).shift():this.getBaseUrl()+this.options.src.split(/([^\/]*)$/gi).shift();if(!(e=this.getFullyQualifiedUrl(e,s)).match(/#xywh=/i))return t.background='url("'+e+'")',t;var r=this.getPropsFromDef(e);return t.background='url("'+r.image+'") no-repeat -'+r.x+"px -"+r.y+"px",t.width=r.w+"px",t.height=r.h+"px",t.url=r.image,t},t.deconstructTimestamp=function(e){var t=e.split("."),s=t[0].split(":");return{milliseconds:parseInt(t[1],10)||0,seconds:parseInt(s.pop(),10)||0,minutes:parseInt(s.pop(),10)||0,hours:parseInt(s.pop(),10)||0}},t.getSecondsFromTimestamp=function(e){var t=this.deconstructTimestamp(e);return parseInt(3600*t.hours+60*t.minutes+t.seconds+t.milliseconds/1e3,10)},t.trim=function(e,t){var s=[" ","\n","\r","\t","\f","\v"," "," "," "," "," "," "," "," "," "," "," "," ","","\u2028","\u2029"," "].join(""),r=0,i=0;for(e+="",t&&(s=(t+"").replace(/([[\]().?/*{}+$^:])/g,"$1")),r=e.length,i=0;i<r;i++)if(-1===s.indexOf(e.charAt(i))){e=e.substring(i);break}for(i=(r=e.length)-1;0<=i;i--)if(-1===s.indexOf(e.charAt(i))){e=e.substring(0,i+1);break}return-1===s.indexOf(e.charAt(0))?e:""},e}();return t("vttThumbnails",e),e.VERSION="0.0.13",e});
\ No newline at end of file diff --git a/src/invidious.cr b/src/invidious.cr index 4855eecf..284b238c 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -297,205 +297,8 @@ Invidious::Routing.get "/", Invidious::Routes::Home Invidious::Routing.get "/privacy", Invidious::Routes::Privacy Invidious::Routing.get "/licenses", Invidious::Routes::Licenses Invidious::Routing.get "/watch", Invidious::Routes::Watch - -get "/embed/" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") - begin - playlist = get_playlist(PG_DB, plid, locale: locale) - offset = env.params.query["index"]?.try &.to_i? || 0 - videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - url = "/embed/#{videos[0].id}?#{env.params.query}" - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - else - url = "/" - end - - env.redirect url -end - -get "/embed/:id" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - id = env.params.url["id"] - - plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") - continuation = process_continuation(PG_DB, env.params.query, plid, id) - - if md = env.params.query["playlist"]? - .try &.match(/[a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})*/) - video_series = md[0].split(",") - env.params.query.delete("playlist") - end - - preferences = env.get("preferences").as(Preferences) - - if id.includes?("%20") || id.includes?("+") || env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") - id = env.params.url["id"].gsub("%20", "").delete("+") - - url = "/embed/#{id}" - - if env.params.query.size > 0 - url += "?#{env.params.query.to_s.gsub("%20", "").delete("+")}" - end - - next env.redirect url - end - - # YouTube embed supports `videoseries` with either `list=PLID` - # or `playlist=VIDEO_ID,VIDEO_ID` - case id - when "videoseries" - url = "" - - if plid - begin - playlist = get_playlist(PG_DB, plid, locale: locale) - offset = env.params.query["index"]?.try &.to_i? || 0 - videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - url = "/embed/#{videos[0].id}" - elsif video_series - url = "/embed/#{video_series.shift}" - env.params.query["playlist"] = video_series.join(",") - else - next env.redirect "/" - end - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - - next env.redirect url - when "live_stream" - response = YT_POOL.client &.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}") - video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"] - - env.params.query.delete_all("channel") - - if !video_id || video_id == "live_stream" - error_message = "Video is unavailable." - next templated "error" - end - - url = "/embed/#{video_id}" - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - - next env.redirect url - when id.size > 11 - url = "/embed/#{id[0, 11]}" - - if env.params.query.size > 0 - url += "?#{env.params.query}" - end - - next env.redirect url - else nil # Continue - end - - params = process_video_params(env.params.query, preferences) - - user = env.get?("user").try &.as(User) - if user - subscriptions = user.subscriptions - watched = user.watched - notifications = user.notifications - end - subscriptions ||= [] of String - - begin - video = get_video(id, PG_DB, region: params.region) - rescue ex : VideoRedirect - next env.redirect env.request.resource.gsub(id, ex.video_id) - rescue ex - error_message = ex.message - env.response.status_code = 500 - next templated "error" - end - - if preferences.annotations_subscribed && - subscriptions.includes?(video.ucid) && - (env.params.query["iv_load_policy"]? || "1") == "1" - params.annotations = true - end - - # if watched && !watched.includes? id - # PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) - # end - - if notifications && notifications.includes? id - PG_DB.exec("UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2", id, user.as(User).email) - env.get("user").as(User).notifications.delete(id) - notifications.delete(id) - end - - fmt_stream = video.fmt_stream - adaptive_fmts = video.adaptive_fmts - - if params.local - fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } - adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } - end - - video_streams = video.video_streams - audio_streams = video.audio_streams - - if audio_streams.empty? && !video.live_now - if params.quality == "dash" - env.params.query.delete_all("quality") - next env.redirect "/embed/#{id}?#{env.params.query}" - elsif params.listen - env.params.query.delete_all("listen") - env.params.query["listen"] = "0" - next env.redirect "/embed/#{id}?#{env.params.query}" - end - end - - captions = video.captions - - preferred_captions = captions.select { |caption| - params.preferred_captions.includes?(caption.name.simpleText) || - params.preferred_captions.includes?(caption.languageCode.split("-")[0]) - } - preferred_captions.sort_by! { |caption| - (params.preferred_captions.index(caption.name.simpleText) || - params.preferred_captions.index(caption.languageCode.split("-")[0])).not_nil! - } - captions = captions - preferred_captions - - aspect_ratio = nil - - thumbnail = "/vi/#{video.id}/maxres.jpg" - - if params.raw - url = fmt_stream[0]["url"].as_s - - fmt_stream.each do |fmt| - url = fmt["url"].as_s if fmt["quality"].as_s == params.quality - end - - next env.redirect url - end - - rendered "embed" -end +Invidious::Routing.get "/embed/", Invidious::Routes::Embed::Index +Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed::Show # Playlists diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index dce9e6aa..656b9953 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -887,8 +887,8 @@ def get_about_info(ucid, locale) # Auto-generated channels # https://support.google.com/youtube/answer/2579942 # For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] - if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) - (channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube" + if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) && + (channel_about_meta["primaryLinks"][0]["title"]?.try &.["simpleText"]?.try &.as_s? || "") == "Auto-generated by YouTube" auto_generated = true end end diff --git a/src/invidious/routes/embed/index.cr b/src/invidious/routes/embed/index.cr new file mode 100644 index 00000000..79c91d86 --- /dev/null +++ b/src/invidious/routes/embed/index.cr @@ -0,0 +1,27 @@ +class Invidious::Routes::Embed::Index < Invidious::Routes::BaseRoute + def handle(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") + begin + playlist = get_playlist(PG_DB, plid, locale: locale) + offset = env.params.query["index"]?.try &.to_i? || 0 + videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + url = "/embed/#{videos[0].id}?#{env.params.query}" + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + else + url = "/" + end + + env.redirect url + end +end diff --git a/src/invidious/routes/embed/show.cr b/src/invidious/routes/embed/show.cr new file mode 100644 index 00000000..23c2b86f --- /dev/null +++ b/src/invidious/routes/embed/show.cr @@ -0,0 +1,174 @@ +class Invidious::Routes::Embed::Show < Invidious::Routes::BaseRoute + def handle(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + id = env.params.url["id"] + + plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") + continuation = process_continuation(PG_DB, env.params.query, plid, id) + + if md = env.params.query["playlist"]? + .try &.match(/[a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})*/) + video_series = md[0].split(",") + env.params.query.delete("playlist") + end + + preferences = env.get("preferences").as(Preferences) + + if id.includes?("%20") || id.includes?("+") || env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") + id = env.params.url["id"].gsub("%20", "").delete("+") + + url = "/embed/#{id}" + + if env.params.query.size > 0 + url += "?#{env.params.query.to_s.gsub("%20", "").delete("+")}" + end + + return env.redirect url + end + + # YouTube embed supports `videoseries` with either `list=PLID` + # or `playlist=VIDEO_ID,VIDEO_ID` + case id + when "videoseries" + url = "" + + if plid + begin + playlist = get_playlist(PG_DB, plid, locale: locale) + offset = env.params.query["index"]?.try &.to_i? || 0 + videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + url = "/embed/#{videos[0].id}" + elsif video_series + url = "/embed/#{video_series.shift}" + env.params.query["playlist"] = video_series.join(",") + else + return env.redirect "/" + end + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + + return env.redirect url + when "live_stream" + response = YT_POOL.client &.get("/embed/live_stream?channel=#{env.params.query["channel"]? || ""}") + video_id = response.body.match(/"video_id":"(?<video_id>[a-zA-Z0-9_-]{11})"/).try &.["video_id"] + + env.params.query.delete_all("channel") + + if !video_id || video_id == "live_stream" + error_message = "Video is unavailable." + return templated "error" + end + + url = "/embed/#{video_id}" + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + + return env.redirect url + when id.size > 11 + url = "/embed/#{id[0, 11]}" + + if env.params.query.size > 0 + url += "?#{env.params.query}" + end + + return env.redirect url + else nil # Continue + end + + params = process_video_params(env.params.query, preferences) + + user = env.get?("user").try &.as(User) + if user + subscriptions = user.subscriptions + watched = user.watched + notifications = user.notifications + end + subscriptions ||= [] of String + + begin + video = get_video(id, PG_DB, region: params.region) + rescue ex : VideoRedirect + return env.redirect env.request.resource.gsub(id, ex.video_id) + rescue ex + error_message = ex.message + env.response.status_code = 500 + return templated "error" + end + + if preferences.annotations_subscribed && + subscriptions.includes?(video.ucid) && + (env.params.query["iv_load_policy"]? || "1") == "1" + params.annotations = true + end + + # if watched && !watched.includes? id + # PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) + # end + + if notifications && notifications.includes? id + PG_DB.exec("UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2", id, user.as(User).email) + env.get("user").as(User).notifications.delete(id) + notifications.delete(id) + end + + fmt_stream = video.fmt_stream + adaptive_fmts = video.adaptive_fmts + + if params.local + fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } + adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } + end + + video_streams = video.video_streams + audio_streams = video.audio_streams + + if audio_streams.empty? && !video.live_now + if params.quality == "dash" + env.params.query.delete_all("quality") + return env.redirect "/embed/#{id}?#{env.params.query}" + elsif params.listen + env.params.query.delete_all("listen") + env.params.query["listen"] = "0" + return env.redirect "/embed/#{id}?#{env.params.query}" + end + end + + captions = video.captions + + preferred_captions = captions.select { |caption| + params.preferred_captions.includes?(caption.name.simpleText) || + params.preferred_captions.includes?(caption.languageCode.split("-")[0]) + } + preferred_captions.sort_by! { |caption| + (params.preferred_captions.index(caption.name.simpleText) || + params.preferred_captions.index(caption.languageCode.split("-")[0])).not_nil! + } + captions = captions - preferred_captions + + aspect_ratio = nil + + thumbnail = "/vi/#{video.id}/maxres.jpg" + + if params.raw + url = fmt_stream[0]["url"].as_s + + fmt_stream.each do |fmt| + url = fmt["url"].as_s if fmt["quality"].as_s == params.quality + end + + return env.redirect url + end + + rendered "embed" + end +end diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index 8162546e..d02f82d2 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -3,6 +3,7 @@ <link rel="stylesheet" href="/css/videojs.markers.min.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/videojs-share.css?v=<%= ASSET_COMMIT %>"> <link rel="stylesheet" href="/css/videojs-vtt-thumbnails.css?v=<%= ASSET_COMMIT %>"> +<link rel="stylesheet" href="/css/videojs-vtt-thumbnails-fix.css?v=<%= ASSET_COMMIT %>"> <script src="/js/global.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/video.min.js?v=<%= ASSET_COMMIT %>"></script> <script src="/js/videojs-contrib-quality-levels.min.js?v=<%= ASSET_COMMIT %>"></script> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index d084bd31..bc13b7ea 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -2,6 +2,24 @@ <title><%= search_query.not_nil!.size > 30 ? HTML.escape(query.not_nil![0,30].rstrip(".") + "...") : HTML.escape(query.not_nil!) %> - Invidious</title> <% end %> +<div class="pure-g h-box v-box"> + <div class="pure-u-1 pure-u-lg-1-5"> + <% if page > 1 %> + <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page - 1 %>"> + <%= translate(locale, "Previous page") %> + </a> + <% end %> + </div> + <div class="pure-u-1 pure-u-lg-3-5"></div> + <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> + <% if count >= 20 %> + <a href="/search?q=<%= HTML.escape(query.not_nil!) %>&page=<%= page + 1 %>"> + <%= translate(locale, "Next page") %> + </a> + <% end %> + </div> +</div> + <div class="pure-g"> <% videos.each_slice(4) do |slice| %> <% slice.each do |item| %> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 61cf5c3a..abcfbe22 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -4,7 +4,6 @@ <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="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=<%= ASSET_COMMIT %>"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=<%= ASSET_COMMIT %>"> |
