mirror of https://github.com/iv-org/invidious.git
Merge branch 'master' into master
This commit is contained in:
commit
8cd0137aed
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -269,5 +269,12 @@
|
|||
"Top": "",
|
||||
"About": "Über",
|
||||
"Rating: ": "Bewertung: ",
|
||||
"Language: ": "Sprache: "
|
||||
"Language: ": "Sprache: ",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": ""
|
||||
}
|
||||
|
|
|
@ -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: "
|
||||
}
|
||||
|
|
|
@ -263,5 +263,12 @@
|
|||
"Top": "Haut",
|
||||
"About": "Sur",
|
||||
"Rating: ": "Évaluation: ",
|
||||
"Language: ": "Langue: "
|
||||
"Language: ": "Langue: ",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": ""
|
||||
}
|
||||
|
|
|
@ -263,5 +263,12 @@
|
|||
"Top": "Topp",
|
||||
"About": "Om",
|
||||
"Rating: ": "Vurdering: ",
|
||||
"Language: ": "Språk: "
|
||||
"Language: ": "Språk: ",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": ""
|
||||
}
|
||||
|
|
|
@ -263,5 +263,12 @@
|
|||
"Top": "",
|
||||
"About": "",
|
||||
"Rating: ": "",
|
||||
"Language: ": ""
|
||||
"Language: ": "",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": ""
|
||||
}
|
||||
|
|
|
@ -263,5 +263,12 @@
|
|||
"Top": "",
|
||||
"About": "",
|
||||
"Rating: ": "",
|
||||
"Language: ": ""
|
||||
"Language: ": "",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": ""
|
||||
}
|
||||
|
|
|
@ -269,5 +269,12 @@
|
|||
"Top": "Топ",
|
||||
"About": "О сайте",
|
||||
"Rating: ": "Рейтинг: ",
|
||||
"Language: ": "Язык: "
|
||||
"Language: ": "Язык: ",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": ""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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-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>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue