Merge branch 'master' into master
This commit is contained in:
commit
8cd0137aed
15 changed files with 1056 additions and 834 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -275,6 +275,6 @@
|
||||||
"Gaming": "الألعاب",
|
"Gaming": "الألعاب",
|
||||||
"News": "الأخبار",
|
"News": "الأخبار",
|
||||||
"Movies": "الأفلام",
|
"Movies": "الأفلام",
|
||||||
"Download as: ": "تحميل كـ",
|
"Download as:": "تحميل كـ",
|
||||||
"Download": "تحميل"
|
"Download": "تحميل"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: ": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: "
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: ": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: ": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,5 +263,12 @@
|
||||||
"Top": "",
|
"Top": "",
|
||||||
"About": "",
|
"About": "",
|
||||||
"Rating: ": "",
|
"Rating: ": "",
|
||||||
"Language: ": ""
|
"Language: ": "",
|
||||||
|
"Default": "",
|
||||||
|
"Music": "",
|
||||||
|
"Gaming": "",
|
||||||
|
"News": "",
|
||||||
|
"Movies": "",
|
||||||
|
"Download": "",
|
||||||
|
"Download as: ": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -263,5 +263,12 @@
|
||||||
"Top": "",
|
"Top": "",
|
||||||
"About": "",
|
"About": "",
|
||||||
"Rating: ": "",
|
"Rating: ": "",
|
||||||
"Language: ": ""
|
"Language: ": "",
|
||||||
|
"Default": "",
|
||||||
|
"Music": "",
|
||||||
|
"Gaming": "",
|
||||||
|
"News": "",
|
||||||
|
"Movies": "",
|
||||||
|
"Download": "",
|
||||||
|
"Download as: ": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,5 +269,12 @@
|
||||||
"Top": "Топ",
|
"Top": "Топ",
|
||||||
"About": "О сайте",
|
"About": "О сайте",
|
||||||
"Rating: ": "Рейтинг: ",
|
"Rating: ": "Рейтинг: ",
|
||||||
"Language: ": "Язык: "
|
"Language: ": "Язык: ",
|
||||||
|
"Default": "",
|
||||||
|
"Music": "",
|
||||||
|
"Gaming": "",
|
||||||
|
"News": "",
|
||||||
|
"Movies": "",
|
||||||
|
"Download": "",
|
||||||
|
"Download as: ": ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
35
src/invidious/helpers/logger.cr
Normal file
35
src/invidious/helpers/logger.cr
Normal 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
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue