Merge branch 'master' into master

This commit is contained in:
Esmail EL BoB 2019-01-24 11:05:33 +02:00 committed by GitHub
commit 8cd0137aed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1056 additions and 834 deletions

View file

@ -262,8 +262,23 @@ img.thumbnail {
#player-container { #player-container {
position: relative; position: relative;
padding-bottom: 56.25%; padding-bottom: 55.25%;
margin-left: 1em; margin-left: 2em;
margin-right: 1em; margin-right: 2em;
height: 0; height: 0;
} }
#progress-container {
width: 100%;
border-radius: 2px;
background: #aaa;
}
#download-progress {
width: 0%;
border-radius: 2px;
height: 10px;
background-color: #0078e7;
margin-top: 0.5em;
margin-bottom: 0.5em;
}

View file

@ -50,3 +50,59 @@ function hide_youtube_replies(target) {
target.innerHTML = "Show replies"; target.innerHTML = "Show replies";
target.setAttribute("onclick", "show_youtube_replies(this)"); target.setAttribute("onclick", "show_youtube_replies(this)");
} }
function download_video(target) {
var title = target.getAttribute("data-title");
var children = document.getElementById("download_widget").children;
var progress = document.getElementById("download-progress");
var url = "";
document.getElementById("progress-container").style.display = "";
for (i = 0; i < children.length; i++) {
if (children[i].selected) {
url = children[i].getAttribute("data-url");
}
}
url = "/videoplayback" + url.split("/videoplayback")[1];
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "arraybuffer";
xhr.onprogress = function(event) {
if (event.lengthComputable) {
progress.style.width = "" + (event.loaded / event.total)*100 + "%";
}
};
xhr.onload = function(event) {
if (event.currentTarget.status != 200) {
console.log("Downloading " + title + " failed.")
document.getElementById("progress-container").style.display = "none";
progress.style.width = "0%";
return;
}
var data = new Blob([xhr.response], {'type' : 'video/mp4'});
var videoFile = window.URL.createObjectURL(data);
var link = document.createElement('a');
link.href = videoFile;
link.setAttribute('download', title);
document.body.appendChild(link);
window.requestAnimationFrame(function() {
var event = new MouseEvent('click');
link.dispatchEvent(event);
document.body.removeChild(link);
});
document.getElementById("progress-container").style.display = "none";
progress.style.width = "0%";
};
xhr.send(null);
}

View file

@ -269,5 +269,12 @@
"Top": "", "Top": "",
"About": "Über", "About": "Über",
"Rating: ": "Bewertung: ", "Rating: ": "Bewertung: ",
"Language: ": "Sprache: " "Language: ": "Sprache: ",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
} }

View file

@ -263,5 +263,12 @@
"Top": "Top", "Top": "Top",
"About": "About", "About": "About",
"Rating: ": "Rating: ", "Rating: ": "Rating: ",
"Language: ": "Language: " "Language: ": "Language: ",
"Default": "Default",
"Music": "Music",
"Gaming": "Gaming",
"News": "News",
"Movies": "Movies",
"Download": "Download",
"Download as: ": "Download as: "
} }

View file

@ -263,5 +263,12 @@
"Top": "Haut", "Top": "Haut",
"About": "Sur", "About": "Sur",
"Rating: ": "Évaluation: ", "Rating: ": "Évaluation: ",
"Language: ": "Langue: " "Language: ": "Langue: ",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
} }

View file

@ -263,5 +263,12 @@
"Top": "Topp", "Top": "Topp",
"About": "Om", "About": "Om",
"Rating: ": "Vurdering: ", "Rating: ": "Vurdering: ",
"Language: ": "Språk: " "Language: ": "Språk: ",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
} }

View file

@ -263,5 +263,12 @@
"Top": "", "Top": "",
"About": "", "About": "",
"Rating: ": "", "Rating: ": "",
"Language: ": "" "Language: ": "",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
} }

View file

@ -263,5 +263,12 @@
"Top": "", "Top": "",
"About": "", "About": "",
"Rating: ": "", "Rating: ": "",
"Language: ": "" "Language: ": "",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
} }

View file

@ -269,5 +269,12 @@
"Top": "Топ", "Top": "Топ",
"About": "О сайте", "About": "О сайте",
"Rating: ": "Рейтинг: ", "Rating: ": "Рейтинг: ",
"Language: ": "Язык: " "Language: ": "Язык: ",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
} }

View file

@ -16,6 +16,7 @@
require "detect_language" require "detect_language"
require "digest/md5" require "digest/md5"
require "file_utils"
require "kemal" require "kemal"
require "openssl/hmac" require "openssl/hmac"
require "option_parser" require "option_parser"
@ -35,6 +36,8 @@ channel_threads = CONFIG.channel_threads
feed_threads = CONFIG.feed_threads feed_threads = CONFIG.feed_threads
video_threads = CONFIG.video_threads video_threads = CONFIG.video_threads
logger = Invidious::LogHandler.new
Kemal.config.extra_options do |parser| Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]" parser.banner = "Usage: invidious [arguments]"
parser.on("-t THREADS", "--crawl-threads=THREADS", "Number of threads for crawling YouTube (default: #{crawl_threads})") do |number| parser.on("-t THREADS", "--crawl-threads=THREADS", "Number of threads for crawling YouTube (default: #{crawl_threads})") do |number|
@ -69,6 +72,10 @@ Kemal.config.extra_options do |parser|
exit exit
end end
end end
parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: STDOUT)") do |output|
FileUtils.mkdir_p(File.dirname(output))
logger = Invidious::LogHandler.new(File.open(output, mode: "a"))
end
end end
Kemal::CLI.new Kemal::CLI.new
@ -295,7 +302,7 @@ get "/watch" do |env|
next env.redirect "/watch?v=#{ex.message}" next env.redirect "/watch?v=#{ex.message}"
rescue ex rescue ex
error_message = ex.message error_message = ex.message
STDOUT << id << " : " << ex.message << "\n" logger.write("#{id} : #{ex.message}\n")
next templated "error" next templated "error"
end end
@ -2135,6 +2142,16 @@ get "/c/:user" do |env|
env.redirect anchor["href"] env.redirect anchor["href"]
end end
# Legacy endpoint for /user/:username
get "/profile" do |env|
user = env.params.query["user"]?
if !user
env.redirect "/"
else
env.redirect "/user/#{user}"
end
end
get "/user/:user" do |env| get "/user/:user" do |env|
user = env.params.url["user"] user = env.params.url["user"]
env.redirect "/channel/#{user}" env.redirect "/channel/#{user}"
@ -3849,4 +3866,5 @@ add_handler FilteredCompressHandler.new
add_handler DenyFrame.new add_handler DenyFrame.new
add_context_storage_type(User) add_context_storage_type(User)
Kemal.config.logger = logger
Kemal.run Kemal.run

View file

@ -1,21 +1,21 @@
class Config class Config
YAML.mapping({ YAML.mapping({
crawl_threads: Int32, crawl_threads: Int32, # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
channel_threads: Int32, channel_threads: Int32, # Number of threads to use for crawling videos from channels (for updating subscriptions)
feed_threads: Int32, feed_threads: Int32, # Number of threads to use for updating feeds
video_threads: Int32, video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
db: NamedTuple( db: NamedTuple( # Database configuration
user: String, user: String,
password: String, password: String,
host: String, host: String,
port: Int32, port: Int32,
dbname: String, dbname: String,
), ),
dl_api_key: String?, dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional
https_only: Bool?, https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
hmac_key: String?, hmac_key: String?, # HMAC signing key for CSRF tokens
full_refresh: Bool, full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
domain: String, domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
}) })
end end

View file

@ -0,0 +1,35 @@
require "logger"
class Invidious::LogHandler < Kemal::BaseLogHandler
def initialize(@io : IO = STDOUT)
end
def call(context : HTTP::Server::Context)
time = Time.now
call_next(context)
elapsed_text = elapsed_text(Time.now - time)
@io << time << ' ' << context.response.status_code << ' ' << context.request.method << ' ' << context.request.resource << ' ' << elapsed_text << '\n'
if @io.is_a? File
@io.flush
end
context
end
def write(message : String)
@io << message
if @io.is_a? File
@io.flush
end
end
private def elapsed_text(elapsed)
millis = elapsed.total_milliseconds
return "#{millis.round(2)}ms" if millis >= 1
"#{(millis * 1000).round(2)}µs"
end
end

View file

@ -8,7 +8,7 @@
<script src="/js/videojs-markers.min.js"></script> <script src="/js/videojs-markers.min.js"></script>
<script src="/js/videojs-share.min.js"></script> <script src="/js/videojs-share.min.js"></script>
<script src="/js/videojs-http-streaming.min.js"></script> <script src="/js/videojs-http-streaming.min.js"></script>
<% if env.get?("user") && env.get("user").as(User).preferences.quality == "dash" %> <% if params[:quality] == "dash" %>
<script src="/js/dash.mediaplayer.min.js"></script> <script src="/js/dash.mediaplayer.min.js"></script>
<script src="/js/videojs-dash.min.js"></script> <script src="/js/videojs-dash.min.js"></script>
<script src="/js/videojs-contrib-quality-levels.min.js"></script> <script src="/js/videojs-contrib-quality-levels.min.js"></script>

View file

@ -53,6 +53,34 @@
<div class="pure-u-1 pure-u-md-1-5"> <div class="pure-u-1 pure-u-md-1-5">
<div class="h-box"> <div class="h-box">
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p> <p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p>
<form class="pure-form pure-form-stacked">
<div class="pure-control-group">
<label for="download_widget"><%= translate(locale, "Download as: ") %></label>
<select style="width:100%" name="download_widget" id="download_widget">
<% video_streams.each do |option| %>
<option data-url="<%= option["url"] %>"><%= option["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only</option>
<% end %>
<% audio_streams.each do |option| %>
<option data-url="<%= option["url"] %>"><%= option["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only</option>
<% end %>
<% fmt_stream.each do |option| %>
<option data-url="<%= option["url"] %>"><%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %></option>
<% end %>
</select>
</div>
<div id="progress-container" style="width:100%; display:none">
<div id="download-progress">
</div>
</div>
<button type="button" data-title="<%= video.title.dump_unquoted %>-<%= video.id %>.mp4" onclick="download_video(this)"
class="pure-button pure-button-primary">
<%= translate(locale, "Download") %>
</button>
</form>
<p><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p> <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-up"></i> <%= number_with_separator(video.likes) %></p>
<p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p> <p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
@ -268,8 +296,15 @@ function unsubscribe() {
} }
<% if plid %> <% if plid %>
function get_playlist() { function get_playlist(timeouts = 0) {
playlist = document.getElementById("playlist"); playlist = document.getElementById("playlist");
if (timeouts > 10) {
console.log("Failed to pull playlist");
playlist.innerHTML = "";
return;
}
playlist.innerHTML = ' \ playlist.innerHTML = ' \
<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3> \ <h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3> \
<hr>' <hr>'
@ -323,15 +358,22 @@ function get_playlist() {
comments = document.getElementById("playlist"); comments = document.getElementById("playlist");
comments.innerHTML = comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3><hr>'; '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3><hr>';
get_playlist(); get_playlist(timeouts + 1);
}; };
} }
get_playlist(); get_playlist();
<% end %> <% end %>
function get_reddit_comments() { function get_reddit_comments(timeouts = 0) {
comments = document.getElementById("comments"); comments = document.getElementById("comments");
if (timeouts > 10) {
console.log("Failed to pull comments");
comments.innerHTML = "";
return;
}
var fallback = comments.innerHTML; var fallback = comments.innerHTML;
comments.innerHTML = comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>'; '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
@ -382,12 +424,19 @@ function get_reddit_comments() {
xhr.ontimeout = function() { xhr.ontimeout = function() {
console.log("Pulling comments timed out."); console.log("Pulling comments timed out.");
get_reddit_comments(); get_reddit_comments(timeouts + 1);
}; };
} }
function get_youtube_comments() { function get_youtube_comments(timeouts = 0) {
comments = document.getElementById("comments"); comments = document.getElementById("comments");
if (timeouts > 10) {
console.log("Failed to pull comments");
comments.innerHTML = "";
return;
}
var fallback = comments.innerHTML; var fallback = comments.innerHTML;
comments.innerHTML = comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>'; '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
@ -438,7 +487,7 @@ function get_youtube_comments() {
comments.innerHTML = comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>'; '<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
get_youtube_comments(); get_youtube_comments(timeouts + 1);
}; };
} }