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 {
position: relative;
padding-bottom: 56.25%;
margin-left: 1em;
margin-right: 1em;
padding-bottom: 55.25%;
margin-left: 2em;
margin-right: 2em;
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.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": "",
"About": "Über",
"Rating: ": "Bewertung: ",
"Language: ": "Sprache: "
"Language: ": "Sprache: ",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
}

View file

@ -263,5 +263,12 @@
"Top": "Top",
"About": "About",
"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",
"About": "Sur",
"Rating: ": "Évaluation: ",
"Language: ": "Langue: "
"Language: ": "Langue: ",
"Default": "",
"Music": "",
"Gaming": "",
"News": "",
"Movies": "",
"Download": "",
"Download as: ": ""
}

View file

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

View file

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

View file

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

View file

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

View file

@ -16,6 +16,7 @@
require "detect_language"
require "digest/md5"
require "file_utils"
require "kemal"
require "openssl/hmac"
require "option_parser"
@ -35,6 +36,8 @@ channel_threads = CONFIG.channel_threads
feed_threads = CONFIG.feed_threads
video_threads = CONFIG.video_threads
logger = Invidious::LogHandler.new
Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]"
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
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
Kemal::CLI.new
@ -295,7 +302,7 @@ get "/watch" do |env|
next env.redirect "/watch?v=#{ex.message}"
rescue ex
error_message = ex.message
STDOUT << id << " : " << ex.message << "\n"
logger.write("#{id} : #{ex.message}\n")
next templated "error"
end
@ -2135,6 +2142,16 @@ get "/c/:user" do |env|
env.redirect anchor["href"]
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|
user = env.params.url["user"]
env.redirect "/channel/#{user}"
@ -3849,4 +3866,5 @@ add_handler FilteredCompressHandler.new
add_handler DenyFrame.new
add_context_storage_type(User)
Kemal.config.logger = logger
Kemal.run

View file

@ -1,21 +1,21 @@
class Config
YAML.mapping({
crawl_threads: Int32,
channel_threads: Int32,
feed_threads: Int32,
video_threads: Int32,
db: NamedTuple(
crawl_threads: Int32, # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
channel_threads: Int32, # Number of threads to use for crawling videos from channels (for updating subscriptions)
feed_threads: Int32, # Number of threads to use for updating feeds
video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
db: NamedTuple( # Database configuration
user: String,
password: String,
host: String,
port: Int32,
dbname: String,
),
dl_api_key: String?,
https_only: Bool?,
hmac_key: String?,
full_refresh: Bool,
domain: String,
dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional
https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
hmac_key: String?, # HMAC signing key for CSRF tokens
full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
})
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-share.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/videojs-dash.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="h-box">
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p>
<form class="pure-form pure-form-stacked">
<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-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
<p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
@ -268,8 +296,15 @@ function unsubscribe() {
}
<% if plid %>
function get_playlist() {
function get_playlist(timeouts = 0) {
playlist = document.getElementById("playlist");
if (timeouts > 10) {
console.log("Failed to pull playlist");
playlist.innerHTML = "";
return;
}
playlist.innerHTML = ' \
<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3> \
<hr>'
@ -323,15 +358,22 @@ function get_playlist() {
comments = document.getElementById("playlist");
comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3><hr>';
get_playlist();
get_playlist(timeouts + 1);
};
}
get_playlist();
<% end %>
function get_reddit_comments() {
function get_reddit_comments(timeouts = 0) {
comments = document.getElementById("comments");
if (timeouts > 10) {
console.log("Failed to pull comments");
comments.innerHTML = "";
return;
}
var fallback = comments.innerHTML;
comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
@ -382,12 +424,19 @@ function get_reddit_comments() {
xhr.ontimeout = function() {
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");
if (timeouts > 10) {
console.log("Failed to pull comments");
comments.innerHTML = "";
return;
}
var fallback = comments.innerHTML;
comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
@ -438,7 +487,7 @@ function get_youtube_comments() {
comments.innerHTML =
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
get_youtube_comments();
get_youtube_comments(timeouts + 1);
};
}