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