summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--assets/css/videojs-vtt-thumbnails-fix.css3
-rw-r--r--assets/js/player.js3
-rw-r--r--assets/js/videojs-vtt-thumbnails.min.js4
-rw-r--r--src/invidious.cr201
-rw-r--r--src/invidious/channels.cr4
-rw-r--r--src/invidious/routes/embed/index.cr27
-rw-r--r--src/invidious/routes/embed/show.cr174
-rw-r--r--src/invidious/views/components/player_sources.ecr1
-rw-r--r--src/invidious/views/search.ecr18
-rw-r--r--src/invidious/views/template.ecr1
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 %>">