forked from Fijxu/invidious
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
d75d860888
41 changed files with 170 additions and 214 deletions
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -10,8 +10,10 @@ assignees: ''
|
|||
<!--
|
||||
BEFORE TRYING TO REPORT A BUG:
|
||||
|
||||
* Read the FAQ!
|
||||
* Use the search function to check if there is already an issue open for your problem!
|
||||
* Read the FAQ: https://docs.invidious.io/faq/!
|
||||
* Use the search function to check if there is already an issue open for your problem: https://github.com/search?q=repo%3Aiv-org%2Finvidious+replace+me+with+your+bug&type=issues!
|
||||
|
||||
MAKE SURE TO FOLLOW THE TWO STEPS ABOVE BEFORE REPORTING A BUG. A BUG THAT ALREADY EXIST WILL IMMEDIATELY CLOSED.
|
||||
|
||||
If you want to suggest a new feature please use "Feature request" instead
|
||||
If you want to suggest an enhancement to an existing feature please use "Enhancement" instead
|
||||
|
|
13
.github/workflows/build-nightly-container.yml
vendored
13
.github/workflows/build-nightly-container.yml
vendored
|
@ -23,19 +23,6 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1.8.2
|
||||
with:
|
||||
crystal: 1.12.2
|
||||
|
||||
- name: Run lint
|
||||
run: |
|
||||
if ! crystal tool format --check; then
|
||||
crystal tool format
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
|
|
13
.github/workflows/build-stable-container.yml
vendored
13
.github/workflows/build-stable-container.yml
vendored
|
@ -14,19 +14,6 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1.8.2
|
||||
with:
|
||||
crystal: 1.12.2
|
||||
|
||||
- name: Run lint
|
||||
run: |
|
||||
if ! crystal tool format --check; then
|
||||
crystal tool format
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
|
|
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -38,11 +38,10 @@ jobs:
|
|||
matrix:
|
||||
stable: [true]
|
||||
crystal:
|
||||
- 1.10.1
|
||||
- 1.11.2
|
||||
- 1.12.1
|
||||
- 1.13.2
|
||||
- 1.14.0
|
||||
- 1.15.0
|
||||
include:
|
||||
- crystal: nightly
|
||||
stable: false
|
||||
|
@ -136,6 +135,7 @@ jobs:
|
|||
submodules: true
|
||||
|
||||
- name: Install Crystal
|
||||
id: lint_step_install_crystal
|
||||
uses: crystal-lang/install-crystal@v1.8.0
|
||||
with:
|
||||
crystal: latest
|
||||
|
@ -146,7 +146,7 @@ jobs:
|
|||
path: |
|
||||
./lib
|
||||
./bin
|
||||
key: shards-${{ hashFiles('shard.lock') }}
|
||||
key: shards-${{ hashFiles('shard.lock') }}-${{ steps.lint_step_install_crystal.outputs.crystal }}
|
||||
|
||||
- name: Install Shards
|
||||
run: |
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
var count = document.getElementById('count');
|
||||
count.textContent--;
|
||||
|
||||
var url = '/token_ajax?action_revoke_token=1&redirect=false' +
|
||||
var url = '/token_ajax?action=revoke_token&redirect=false' +
|
||||
'&referer=' + encodeURIComponent(location.href) +
|
||||
'&session=' + target.getAttribute('data-session');
|
||||
|
||||
|
@ -111,7 +111,7 @@
|
|||
var count = document.getElementById('count');
|
||||
count.textContent--;
|
||||
|
||||
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
||||
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
|
||||
'&referer=' + encodeURIComponent(location.href) +
|
||||
'&c=' + target.getAttribute('data-ucid');
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ function add_playlist_video(target) {
|
|||
var select = target.parentNode.children[0].children[1];
|
||||
var option = select.children[select.selectedIndex];
|
||||
|
||||
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
||||
var url = '/playlist_ajax?action=add_video&redirect=false' +
|
||||
'&video_id=' + target.getAttribute('data-id') +
|
||||
'&playlist_id=' + option.getAttribute('data-plid');
|
||||
|
||||
|
@ -21,7 +21,7 @@ function add_playlist_item(target) {
|
|||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||
tile.style.display = 'none';
|
||||
|
||||
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
||||
var url = '/playlist_ajax?action=add_video&redirect=false' +
|
||||
'&video_id=' + target.getAttribute('data-id') +
|
||||
'&playlist_id=' + target.getAttribute('data-plid');
|
||||
|
||||
|
@ -36,7 +36,7 @@ function remove_playlist_item(target) {
|
|||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||
tile.style.display = 'none';
|
||||
|
||||
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
|
||||
var url = '/playlist_ajax?action=remove_video&redirect=false' +
|
||||
'&set_video_id=' + target.getAttribute('data-index') +
|
||||
'&playlist_id=' + target.getAttribute('data-plid');
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ function subscribe() {
|
|||
subscribe_button.onclick = unsubscribe;
|
||||
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||
|
||||
var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
|
||||
var url = '/subscription_ajax?action=create_subscription_to_channel&redirect=false' +
|
||||
'&c=' + subscribe_data.ucid;
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
|
||||
|
@ -32,7 +32,7 @@ function unsubscribe() {
|
|||
subscribe_button.onclick = subscribe;
|
||||
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||
|
||||
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
||||
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
|
||||
'&c=' + subscribe_data.ucid;
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
|
||||
|
|
|
@ -67,6 +67,10 @@ function get_playlist(plid) {
|
|||
'&format=html&hl=' + video_data.preferences.locale;
|
||||
}
|
||||
|
||||
if (video_data.params.listen) {
|
||||
plid_url += '&listen=1'
|
||||
}
|
||||
|
||||
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
|
||||
on200: function (response) {
|
||||
playlist.innerHTML = response.playlistHtml;
|
||||
|
|
|
@ -6,7 +6,7 @@ function mark_watched(target) {
|
|||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||
tile.style.display = 'none';
|
||||
|
||||
var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
|
||||
var url = '/watch_ajax?action=mark_watched&redirect=false' +
|
||||
'&id=' + target.getAttribute('data-id');
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload}, {
|
||||
|
@ -22,7 +22,7 @@ function mark_unwatched(target) {
|
|||
var count = document.getElementById('count');
|
||||
count.textContent--;
|
||||
|
||||
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
|
||||
var url = '/watch_ajax?action=mark_unwatched&redirect=false' +
|
||||
'&id=' + target.getAttribute('data-id');
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload}, {
|
||||
|
|
|
@ -225,11 +225,11 @@ https_only: false
|
|||
##
|
||||
## If unset, then no HTTP proxy will be used.
|
||||
##
|
||||
http_proxy:
|
||||
user:
|
||||
password:
|
||||
host:
|
||||
port:
|
||||
#http_proxy:
|
||||
# user:
|
||||
# password:
|
||||
# host:
|
||||
# port:
|
||||
|
||||
|
||||
##
|
||||
|
|
|
@ -307,6 +307,9 @@ class Config
|
|||
config = Config.from_yaml(config_yaml)
|
||||
|
||||
# Update config from env vars (upcased and prefixed with "INVIDIOUS_")
|
||||
#
|
||||
# Also checks if any top-level config options are set to "CHANGE_ME!!"
|
||||
# TODO: Support non-top-level config options such as the ones in DBConfig
|
||||
{% for ivar in Config.instance_vars %}
|
||||
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
|
||||
|
||||
|
@ -343,6 +346,12 @@ class Config
|
|||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
# Warn when any config attribute is set to "CHANGE_ME!!"
|
||||
if config.{{ivar.id}} == "CHANGE_ME!!"
|
||||
puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!"
|
||||
exit(1)
|
||||
end
|
||||
{% end %}
|
||||
|
||||
if config.invidious_companion.present?
|
||||
|
@ -367,9 +376,6 @@ class Config
|
|||
if config.hmac_key.empty?
|
||||
puts "Config: 'hmac_key' is required/can't be empty"
|
||||
exit(1)
|
||||
elsif config.hmac_key == "CHANGE_ME!!"
|
||||
puts "Config: The value of 'hmac_key' needs to be changed!!"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Build database_url from db.* if it's not set directly
|
||||
|
|
|
@ -13,7 +13,7 @@ module Invidious::Frontend::WatchPage
|
|||
@full_videos,
|
||||
@video_streams,
|
||||
@audio_streams,
|
||||
@captions
|
||||
@captions,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,40 +18,6 @@ end
|
|||
class HTTP::Client
|
||||
property family : Socket::Family = Socket::Family::UNSPEC
|
||||
|
||||
# Override stdlib to automatically initialize proxy if configured
|
||||
#
|
||||
# Accurate as of crystal 1.12.1
|
||||
|
||||
def initialize(@host : String, port = nil, tls : TLSContext = nil)
|
||||
check_host_only(@host)
|
||||
|
||||
{% if flag?(:without_openssl) %}
|
||||
if tls
|
||||
raise "HTTP::Client TLS is disabled because `-D without_openssl` was passed at compile time"
|
||||
end
|
||||
@tls = nil
|
||||
{% else %}
|
||||
@tls = case tls
|
||||
when true
|
||||
OpenSSL::SSL::Context::Client.new
|
||||
when OpenSSL::SSL::Context::Client
|
||||
tls
|
||||
when false, nil
|
||||
nil
|
||||
end
|
||||
{% end %}
|
||||
|
||||
@port = (port || (@tls ? 443 : 80)).to_i
|
||||
|
||||
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
||||
end
|
||||
|
||||
def initialize(@io : IO, @host = "", @port = 80)
|
||||
@reconnect = false
|
||||
|
||||
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
||||
end
|
||||
|
||||
private def io
|
||||
io = @io
|
||||
return io if io
|
||||
|
|
|
@ -130,7 +130,7 @@ def error_json_helper(
|
|||
env : HTTP::Server::Context,
|
||||
status_code : Int32,
|
||||
exception : Exception,
|
||||
additional_fields : Hash(String, Object) | Nil = nil
|
||||
additional_fields : Hash(String, Object) | Nil = nil,
|
||||
)
|
||||
if exception.is_a?(InfoException)
|
||||
return error_json_helper(env, status_code, exception.message || "", additional_fields)
|
||||
|
@ -152,7 +152,7 @@ def error_json_helper(
|
|||
env : HTTP::Server::Context,
|
||||
status_code : Int32,
|
||||
message : String,
|
||||
additional_fields : Hash(String, Object) | Nil = nil
|
||||
additional_fields : Hash(String, Object) | Nil = nil,
|
||||
)
|
||||
env.response.content_type = "application/json"
|
||||
env.response.status_code = status_code
|
||||
|
|
|
@ -27,6 +27,7 @@ class Kemal::RouteHandler
|
|||
# Processes the route if it's a match. Otherwise renders 404.
|
||||
private def process_request(context)
|
||||
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
|
||||
return if context.response.closed?
|
||||
content = context.route.handler.call(context)
|
||||
|
||||
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)
|
||||
|
|
|
@ -24,6 +24,7 @@ struct SearchVideo
|
|||
property length_seconds : Int32
|
||||
property premiere_timestamp : Time?
|
||||
property author_verified : Bool
|
||||
property author_thumbnail : String?
|
||||
property badges : VideoBadges
|
||||
|
||||
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
||||
|
@ -88,6 +89,24 @@ struct SearchVideo
|
|||
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||
json.field "authorVerified", self.author_verified
|
||||
|
||||
author_thumbnail = self.author_thumbnail
|
||||
|
||||
if author_thumbnail
|
||||
json.field "authorThumbnails" do
|
||||
json.array do
|
||||
qualities = {32, 48, 76, 100, 176, 512}
|
||||
|
||||
qualities.each do |quality|
|
||||
json.object do
|
||||
json.field "url", author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
|
||||
json.field "width", quality
|
||||
json.field "height", quality
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
json.field "videoThumbnails" do
|
||||
Invidious::JSONify::APIv1.thumbnails(json, self.id)
|
||||
end
|
||||
|
@ -223,7 +242,7 @@ struct SearchChannel
|
|||
|
||||
qualities.each do |quality|
|
||||
json.object do
|
||||
json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
|
||||
json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
|
||||
json.field "width", quality
|
||||
json.field "height", quality
|
||||
end
|
||||
|
|
|
@ -267,6 +267,12 @@ module Invidious::JSONify::APIv1
|
|||
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
|
||||
json.field "viewCountText", rv["short_view_count"]?
|
||||
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
|
||||
json.field "published", rv["published"]?
|
||||
if !rv["published"]?.nil?
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
|
||||
else
|
||||
json.field "publishedText", ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,7 +81,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
|
|||
})
|
||||
end
|
||||
|
||||
def template_mix(mix)
|
||||
def template_mix(mix, listen)
|
||||
html = <<-END_HTML
|
||||
<h3>
|
||||
<a href="/mix?list=#{mix["mixId"]}">
|
||||
|
@ -95,7 +95,7 @@ def template_mix(mix)
|
|||
mix["videos"].as_a.each do |video|
|
||||
html += <<-END_HTML
|
||||
<li class="pure-menu-item">
|
||||
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
|
||||
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}#{listen ? "&listen=1" : ""}">
|
||||
<div class="thumbnail">
|
||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
||||
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||
|
|
|
@ -505,7 +505,7 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
|
|||
return videos
|
||||
end
|
||||
|
||||
def template_playlist(playlist)
|
||||
def template_playlist(playlist, listen)
|
||||
html = <<-END_HTML
|
||||
<h3>
|
||||
<a href="/playlist?list=#{playlist["playlistId"]}">
|
||||
|
@ -519,7 +519,7 @@ def template_playlist(playlist)
|
|||
playlist["videos"].as_a.each do |video|
|
||||
html += <<-END_HTML
|
||||
<li class="pure-menu-item" id="#{video["videoId"]}">
|
||||
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
|
||||
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}#{listen ? "&listen=1" : ""}">
|
||||
<div class="thumbnail">
|
||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
||||
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||
|
|
|
@ -326,17 +326,9 @@ module Invidious::Routes::Account
|
|||
end
|
||||
end
|
||||
|
||||
if env.params.query["action_revoke_token"]?
|
||||
action = "action_revoke_token"
|
||||
else
|
||||
return env.redirect referer
|
||||
end
|
||||
|
||||
session = env.params.query["session"]?
|
||||
session ||= ""
|
||||
|
||||
case action
|
||||
when .starts_with? "action_revoke_token"
|
||||
case action = env.params.query["action"]?
|
||||
when "revoke_token"
|
||||
session = env.params.query["session"]
|
||||
Invidious::Database::SessionIDs.delete(sid: session, email: user.email)
|
||||
else
|
||||
return error_json(400, "Unsupported action #{action}")
|
||||
|
|
|
@ -75,17 +75,23 @@ module Invidious::Routes::API::Manifest
|
|||
# OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
|
||||
next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
|
||||
|
||||
audio_track = fmt["audioTrack"]?.try &.as_h? || {} of String => JSON::Any
|
||||
lang = audio_track["id"]?.try &.as_s.split('.')[0] || "und"
|
||||
is_default = audio_track.has_key?("audioIsDefault") ? audio_track["audioIsDefault"].as_bool : i == 0
|
||||
displayname = audio_track["displayName"]?.try &.as_s || "Unknown"
|
||||
bitrate = fmt["bitrate"]
|
||||
|
||||
# Different representations of the same audio should be groupped into one AdaptationSet.
|
||||
# However, most players don't support auto quality switching, so we have to trick them
|
||||
# into providing a quality selector.
|
||||
# See https://github.com/iv-org/invidious/issues/3074 for more details.
|
||||
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: fmt["bitrate"].to_s + "k") do
|
||||
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: "#{displayname} [#{bitrate}k]", lang: lang) do
|
||||
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
|
||||
bandwidth = fmt["bitrate"].as_i
|
||||
itag = fmt["itag"].as_i
|
||||
url = fmt["url"].as_s
|
||||
|
||||
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: i == 0 ? "main" : "alternate")
|
||||
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: is_default ? "main" : "alternate")
|
||||
|
||||
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
|
||||
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
|
||||
|
@ -182,8 +188,9 @@ module Invidious::Routes::API::Manifest
|
|||
manifest = response.body
|
||||
|
||||
if local
|
||||
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
|
||||
path = URI.parse(match).path
|
||||
manifest = manifest.gsub(/https:\/\/[^\n"]*/m) do |match|
|
||||
uri = URI.parse(match)
|
||||
path = uri.path
|
||||
|
||||
path = path.lchop("/videoplayback/")
|
||||
path = path.rchop("/")
|
||||
|
@ -212,7 +219,7 @@ module Invidious::Routes::API::Manifest
|
|||
raw_params["fvip"] = fvip["fvip"]
|
||||
end
|
||||
|
||||
raw_params["local"] = "true"
|
||||
raw_params["host"] = uri.host.not_nil!
|
||||
|
||||
proxy = Invidious::HttpServer::Utils.get_external_proxy
|
||||
|
||||
|
|
|
@ -42,6 +42,9 @@ module Invidious::Routes::API::V1::Misc
|
|||
format = env.params.query["format"]?
|
||||
format ||= "json"
|
||||
|
||||
listen_param = env.params.query["listen"]?
|
||||
listen = (listen_param == "true" || listen_param == "1")
|
||||
|
||||
if plid.starts_with? "RD"
|
||||
return env.redirect "/api/v1/mixes/#{plid}"
|
||||
end
|
||||
|
@ -85,7 +88,7 @@ module Invidious::Routes::API::V1::Misc
|
|||
end
|
||||
|
||||
if format == "html"
|
||||
playlist_html = template_playlist(json_response)
|
||||
playlist_html = template_playlist(json_response, listen)
|
||||
index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
|
||||
|
||||
response = {
|
||||
|
@ -111,6 +114,9 @@ module Invidious::Routes::API::V1::Misc
|
|||
format = env.params.query["format"]?
|
||||
format ||= "json"
|
||||
|
||||
listen_param = env.params.query["listen"]?
|
||||
listen = (listen_param == "true" || listen_param == "1")
|
||||
|
||||
begin
|
||||
mix = fetch_mix(rdid, continuation, locale: locale)
|
||||
|
||||
|
@ -141,9 +147,7 @@ module Invidious::Routes::API::V1::Misc
|
|||
json.field "authorUrl", "/channel/#{video.ucid}"
|
||||
|
||||
json.field "videoThumbnails" do
|
||||
json.array do
|
||||
Invidious::JSONify::APIv1.thumbnails(json, video.id)
|
||||
end
|
||||
Invidious::JSONify::APIv1.thumbnails(json, video.id)
|
||||
end
|
||||
|
||||
json.field "index", video.index
|
||||
|
@ -157,7 +161,7 @@ module Invidious::Routes::API::V1::Misc
|
|||
|
||||
if format == "html"
|
||||
response = JSON.parse(response)
|
||||
playlist_html = template_mix(response)
|
||||
playlist_html = template_mix(response, listen)
|
||||
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
|
||||
|
||||
response = {
|
||||
|
|
|
@ -194,6 +194,7 @@ module Invidious::Routes::Feeds
|
|||
length_seconds: 0,
|
||||
premiere_timestamp: nil,
|
||||
author_verified: false,
|
||||
author_thumbnail: nil,
|
||||
badges: VideoBadges::None,
|
||||
})
|
||||
end
|
||||
|
|
|
@ -304,23 +304,6 @@ module Invidious::Routes::Playlists
|
|||
end
|
||||
end
|
||||
|
||||
if env.params.query["action_create_playlist"]?
|
||||
action = "action_create_playlist"
|
||||
elsif env.params.query["action_delete_playlist"]?
|
||||
action = "action_delete_playlist"
|
||||
elsif env.params.query["action_edit_playlist"]?
|
||||
action = "action_edit_playlist"
|
||||
elsif env.params.query["action_add_video"]?
|
||||
action = "action_add_video"
|
||||
video_id = env.params.query["video_id"]
|
||||
elsif env.params.query["action_remove_video"]?
|
||||
action = "action_remove_video"
|
||||
elsif env.params.query["action_move_video_before"]?
|
||||
action = "action_move_video_before"
|
||||
else
|
||||
return env.redirect referer
|
||||
end
|
||||
|
||||
begin
|
||||
playlist_id = env.params.query["playlist_id"]
|
||||
playlist = get_playlist(playlist_id).as(InvidiousPlaylist)
|
||||
|
@ -335,12 +318,8 @@ module Invidious::Routes::Playlists
|
|||
end
|
||||
end
|
||||
|
||||
email = user.email
|
||||
|
||||
case action
|
||||
when "action_edit_playlist"
|
||||
# TODO: Playlist stub
|
||||
when "action_add_video"
|
||||
case action = env.params.query["action"]?
|
||||
when "add_video"
|
||||
if playlist.index.size >= CONFIG.playlist_length_limit
|
||||
if redirect
|
||||
return error_template(400, "Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
|
||||
|
@ -377,12 +356,14 @@ module Invidious::Routes::Playlists
|
|||
|
||||
Invidious::Database::PlaylistVideos.insert(playlist_video)
|
||||
Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
|
||||
when "action_remove_video"
|
||||
when "remove_video"
|
||||
index = env.params.query["set_video_id"]
|
||||
Invidious::Database::PlaylistVideos.delete(index)
|
||||
Invidious::Database::Playlists.update_video_removed(playlist_id, index)
|
||||
when "action_move_video_before"
|
||||
when "move_video_before"
|
||||
# TODO: Playlist stub
|
||||
when nil
|
||||
return error_json(400, "Missing action")
|
||||
else
|
||||
return error_json(400, "Unsupported action #{action}")
|
||||
end
|
||||
|
|
|
@ -32,24 +32,16 @@ module Invidious::Routes::Subscriptions
|
|||
end
|
||||
end
|
||||
|
||||
if env.params.query["action_create_subscription_to_channel"]?.try &.to_i?.try &.== 1
|
||||
action = "action_create_subscription_to_channel"
|
||||
elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1
|
||||
action = "action_remove_subscriptions"
|
||||
else
|
||||
return env.redirect referer
|
||||
end
|
||||
|
||||
channel_id = env.params.query["c"]?
|
||||
channel_id ||= ""
|
||||
|
||||
case action
|
||||
when "action_create_subscription_to_channel"
|
||||
case action = env.params.query["action"]?
|
||||
when "create_subscription_to_channel"
|
||||
if !user.subscriptions.includes? channel_id
|
||||
get_channel(channel_id)
|
||||
Invidious::Database::Users.subscribe_channel(user, channel_id)
|
||||
end
|
||||
when "action_remove_subscriptions"
|
||||
when "remove_subscriptions"
|
||||
Invidious::Database::Users.unsubscribe_channel(user, channel_id)
|
||||
else
|
||||
return error_json(400, "Unsupported action #{action}")
|
||||
|
|
|
@ -169,10 +169,13 @@ module Invidious::Routes::VideoPlayback
|
|||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
if location = resp.headers["Location"]?
|
||||
location = URI.parse(location)
|
||||
location = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||
url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
|
||||
|
||||
env.redirect location
|
||||
if title = query_params["title"]?
|
||||
url = "#{url}&title=#{URI.encode_www_form(title)}"
|
||||
end
|
||||
|
||||
env.redirect url
|
||||
break
|
||||
end
|
||||
|
||||
|
|
|
@ -131,9 +131,6 @@ module Invidious::Routes::Watch
|
|||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||
end
|
||||
|
||||
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||
|
||||
video_streams = video.video_streams
|
||||
audio_streams = video.audio_streams
|
||||
|
||||
|
@ -263,18 +260,10 @@ module Invidious::Routes::Watch
|
|||
end
|
||||
end
|
||||
|
||||
if env.params.query["action_mark_watched"]?
|
||||
action = "action_mark_watched"
|
||||
elsif env.params.query["action_mark_unwatched"]?
|
||||
action = "action_mark_unwatched"
|
||||
else
|
||||
return env.redirect referer
|
||||
end
|
||||
|
||||
case action
|
||||
when "action_mark_watched"
|
||||
case action = env.params.query["action"]?
|
||||
when "mark_watched"
|
||||
Invidious::Database::Users.mark_watched(user, id)
|
||||
when "action_mark_unwatched"
|
||||
when "mark_unwatched"
|
||||
Invidious::Database::Users.mark_unwatched(user, id)
|
||||
else
|
||||
return error_json(400, "Unsupported action #{action}")
|
||||
|
|
|
@ -75,7 +75,7 @@ module Invidious::Search
|
|||
@type : Type = Type::All,
|
||||
@duration : Duration = Duration::None,
|
||||
@features : Features = Features::None,
|
||||
@sort : Sort = Sort::Relevance
|
||||
@sort : Sort = Sort::Relevance,
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ module Invidious::Search
|
|||
def initialize(
|
||||
params : HTTP::Params,
|
||||
@type : Type = Type::Regular,
|
||||
@region : String? = nil
|
||||
@region : String? = nil,
|
||||
)
|
||||
# Get the raw search query string (common to all search types). In
|
||||
# Regular search mode, also look for the `search_query` URL parameter
|
||||
|
|
|
@ -290,42 +290,39 @@ struct Invidious::User
|
|||
end
|
||||
|
||||
def from_newpipe(user : User, body : String) : Bool
|
||||
io = IO::Memory.new(body)
|
||||
Compress::Zip::File.open(IO::Memory.new(body), true) do |file|
|
||||
entry = file.entries.find { |file_entry| file_entry.filename == "newpipe.db" }
|
||||
return false if entry.nil?
|
||||
entry.open do |file_io|
|
||||
# Ensure max size of 4MB
|
||||
io_sized = IO::Sized.new(file_io, 0x400000)
|
||||
|
||||
Compress::Zip::File.open(io) do |file|
|
||||
file.entries.each do |entry|
|
||||
entry.open do |file_io|
|
||||
# Ensure max size of 4MB
|
||||
io_sized = IO::Sized.new(file_io, 0x400000)
|
||||
begin
|
||||
temp = File.tempfile(".db") do |tempfile|
|
||||
begin
|
||||
File.write(tempfile.path, io_sized.gets_to_end)
|
||||
rescue
|
||||
return false
|
||||
end
|
||||
|
||||
next if entry.filename != "newpipe.db"
|
||||
DB.open("sqlite3://" + tempfile.path) do |db|
|
||||
user.watched += db.query_all("SELECT url FROM streams", as: String)
|
||||
.map(&.lchop("https://www.youtube.com/watch?v="))
|
||||
|
||||
tempfile = File.tempfile(".db")
|
||||
user.watched.uniq!
|
||||
Invidious::Database::Users.update_watch_history(user)
|
||||
|
||||
begin
|
||||
File.write(tempfile.path, io_sized.gets_to_end)
|
||||
rescue
|
||||
return false
|
||||
user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
|
||||
.map(&.lchop("https://www.youtube.com/channel/"))
|
||||
|
||||
user.subscriptions.uniq!
|
||||
user.subscriptions = get_batch_channels(user.subscriptions)
|
||||
|
||||
Invidious::Database::Users.update_subscriptions(user)
|
||||
end
|
||||
end
|
||||
|
||||
db = DB.open("sqlite3://" + tempfile.path)
|
||||
|
||||
user.watched += db.query_all("SELECT url FROM streams", as: String)
|
||||
.map(&.lchop("https://www.youtube.com/watch?v="))
|
||||
|
||||
user.watched.uniq!
|
||||
Invidious::Database::Users.update_watch_history(user)
|
||||
|
||||
user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
|
||||
.map(&.lchop("https://www.youtube.com/channel/"))
|
||||
|
||||
user.subscriptions.uniq!
|
||||
user.subscriptions = get_batch_channels(user.subscriptions)
|
||||
|
||||
Invidious::Database::Users.update_subscriptions(user)
|
||||
|
||||
db.close
|
||||
tempfile.delete
|
||||
ensure
|
||||
temp.delete if !temp.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -106,7 +106,7 @@ struct Video
|
|||
if formats = info.dig?("streamingData", "adaptiveFormats")
|
||||
return formats
|
||||
.as_a.map(&.as_h)
|
||||
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
||||
.sort_by! { |f| f["width"]?.try &.as_i || f["audioTrack"]?.try { |a| a["audioIsDefault"]?.try { |v| v.as_bool ? -1 : 0 } } || 0 }
|
||||
else
|
||||
return [] of Hash(String, JSON::Any)
|
||||
end
|
||||
|
|
|
@ -36,6 +36,13 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
|||
|
||||
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
|
||||
|
||||
if published_time_text = related["publishedTimeText"]?
|
||||
decoded_time = decode_date(published_time_text["simpleText"].to_s)
|
||||
published = decoded_time.to_rfc3339.to_s
|
||||
else
|
||||
published = nil
|
||||
end
|
||||
|
||||
# TODO: when refactoring video types, make a struct for related videos
|
||||
# or reuse an existing type, if that fits.
|
||||
return {
|
||||
|
@ -47,6 +54,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
|||
"view_count" => JSON::Any.new(view_count || "0"),
|
||||
"short_view_count" => JSON::Any.new(short_view_count || "0"),
|
||||
"author_verified" => JSON::Any.new(author_verified),
|
||||
"published" => JSON::Any.new(published || ""),
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ module Invidious::Videos
|
|||
|
||||
def initialize(
|
||||
*, @url, @width, @height, @count, @interval,
|
||||
@rows, @columns, @images_count
|
||||
@rows, @columns, @images_count,
|
||||
)
|
||||
authority = /(i\d?).ytimg.com/.match!(@url.host.not_nil!)[1]?
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
|
||||
<div class="top-left-overlay">
|
||||
<%- if env.get? "show_watched" -%>
|
||||
<form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<form data-onsubmit="return_false" action="/watch_ajax?action=mark_watched&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<button type="submit" class="pure-button pure-button-secondary low-profile"
|
||||
data-onclick="mark_watched" data-id="<%= item.id %>">
|
||||
|
@ -141,14 +141,14 @@
|
|||
<%- end -%>
|
||||
|
||||
<%- if plid_form = env.get?("add_playlist_items") -%>
|
||||
<%- form_parameters = "action_add_video=1&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
|
||||
<%- form_parameters = "action=add_video&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
|
||||
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<button type="submit" class="pure-button pure-button-secondary low-profile"
|
||||
data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button>
|
||||
</form>
|
||||
<%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%>
|
||||
<%- form_parameters = "action_remove_video=1&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
|
||||
<%- form_parameters = "action=remove_video&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
|
||||
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<button type="submit" class="pure-button pure-button-secondary low-profile"
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<% if user %>
|
||||
<% if subscriptions.includes? ucid %>
|
||||
<form action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<form action="/subscription_ajax?action=remove_subscriptions&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary">
|
||||
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
|
||||
</button>
|
||||
</form>
|
||||
<% else %>
|
||||
<form action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<form action="/subscription_ajax?action=create_subscription_to_channel&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary">
|
||||
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
</a>
|
||||
|
||||
<div class="top-left-overlay"><div class="watched">
|
||||
<form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<form data-onsubmit="return_false" action="/watch_ajax?action=mark_unwatched&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<button type="submit" class="pure-button pure-button-secondary low-profile"
|
||||
data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<div class="pure-u-2-5"></div>
|
||||
<div class="pure-u-1-5" style="text-align:right">
|
||||
<h3 style="padding-right:0.5em">
|
||||
<form data-onsubmit="return_false" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<form data-onsubmit="return_false" action="/subscription_ajax?action=remove_subscriptions&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= translate(locale, "unsubscribe") %>">
|
||||
</form>
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
<div class="pure-u-1-5" style="text-align:right">
|
||||
<h3 style="padding-right:0.5em">
|
||||
<form data-onsubmit="return_false" action="/token_ajax?action_revoke_token=1&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<form data-onsubmit="return_false" action="/token_ajax?action=revoke_token&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= translate(locale, "revoke") %>">
|
||||
</form>
|
||||
|
|
|
@ -175,7 +175,7 @@ we're going to need to do it here in order to allow for translations.
|
|||
<% if user %>
|
||||
<% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
|
||||
<% if !playlists.empty? %>
|
||||
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax" method="post" target="_blank">
|
||||
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank">
|
||||
<div class="pure-control-group">
|
||||
<label for="playlist_id"><%= translate(locale, "Add to playlist: ") %></label>
|
||||
<select style="width:100%" name="playlist_id" id="playlist_id">
|
||||
|
@ -186,7 +186,6 @@ we're going to need to do it here in order to allow for translations.
|
|||
</div>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<input type="hidden" name="action_add_video" value="1">
|
||||
<input type="hidden" name="video_id" value="<%= video.id %>">
|
||||
<button data-onclick="add_playlist_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary">
|
||||
<b><%= translate(locale, "Add to playlist") %></b>
|
||||
|
|
|
@ -67,6 +67,8 @@ private module Parsers
|
|||
author_id = author_fallback.id
|
||||
end
|
||||
|
||||
author_thumbnail = item_contents.dig?("channelThumbnailSupportedRenderers", "channelThumbnailWithLinkRenderer", "thumbnail", "thumbnails", 0, "url").try &.as_s
|
||||
|
||||
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
|
||||
|
||||
# For live videos (and possibly recently premiered videos) there is no published information.
|
||||
|
@ -148,6 +150,7 @@ private module Parsers
|
|||
length_seconds: length_seconds,
|
||||
premiere_timestamp: premiere_timestamp,
|
||||
author_verified: author_verified,
|
||||
author_thumbnail: author_thumbnail,
|
||||
badges: badges,
|
||||
})
|
||||
end
|
||||
|
@ -579,6 +582,7 @@ private module Parsers
|
|||
length_seconds: duration,
|
||||
premiere_timestamp: Time.unix(0),
|
||||
author_verified: false,
|
||||
author_thumbnail: nil,
|
||||
badges: VideoBadges::None,
|
||||
})
|
||||
end
|
||||
|
@ -708,6 +712,7 @@ private module Parsers
|
|||
length_seconds: duration,
|
||||
premiere_timestamp: Time.unix(0),
|
||||
author_verified: false,
|
||||
author_thumbnail: nil,
|
||||
badges: VideoBadges::None,
|
||||
})
|
||||
end
|
||||
|
@ -1024,7 +1029,7 @@ end
|
|||
def extract_items(
|
||||
initial_data : InitialData,
|
||||
author_fallback : String? = nil,
|
||||
author_id_fallback : String? = nil
|
||||
author_id_fallback : String? = nil,
|
||||
) : {Array(SearchItem), String?}
|
||||
items = [] of SearchItem
|
||||
continuation = nil
|
||||
|
|
|
@ -202,7 +202,7 @@ module YoutubeAPI
|
|||
def initialize(
|
||||
*,
|
||||
@client_type = ClientType::Web,
|
||||
@region = "US"
|
||||
@region = "US",
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -361,7 +361,7 @@ module YoutubeAPI
|
|||
browse_id : String,
|
||||
*, # Force the following parameters to be passed by name
|
||||
params : String,
|
||||
client_config : ClientConfig | Nil = nil
|
||||
client_config : ClientConfig | Nil = nil,
|
||||
)
|
||||
# JSON Request data, required by the API
|
||||
data = {
|
||||
|
@ -552,7 +552,7 @@ module YoutubeAPI
|
|||
def search(
|
||||
search_query : String,
|
||||
params : String,
|
||||
client_config : ClientConfig | Nil = nil
|
||||
client_config : ClientConfig | Nil = nil,
|
||||
)
|
||||
# JSON Request data, required by the API
|
||||
data = {
|
||||
|
@ -578,7 +578,7 @@ module YoutubeAPI
|
|||
|
||||
def get_transcript(
|
||||
params : String,
|
||||
client_config : ClientConfig | Nil = nil
|
||||
client_config : ClientConfig | Nil = nil,
|
||||
) : Hash(String, JSON::Any)
|
||||
data = {
|
||||
"context" => self.make_context(client_config),
|
||||
|
|
Loading…
Add table
Reference in a new issue