mirror of
https://github.com/iv-org/invidious.git
synced 2025-08-03 19:28:29 +00:00
Merge branch 'master' into api-only
This commit is contained in:
commit
507f924c5b
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@ -0,0 +1,28 @@
|
||||
dist: bionic
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: build
|
||||
language: crystal
|
||||
crystal: latest
|
||||
before_install:
|
||||
- shards update
|
||||
- shards install
|
||||
install:
|
||||
- crystal build --error-on-warnings src/invidious.cr
|
||||
script:
|
||||
- crystal tool format --check
|
||||
- crystal spec
|
||||
|
||||
- stage: build_docker
|
||||
language: minimal
|
||||
services:
|
||||
- docker
|
||||
install:
|
||||
- docker-compose build
|
||||
script:
|
||||
- docker-compose up -d
|
||||
- sleep 15 # Wait for cluster to become ready, TODO: do not sleep
|
||||
- HEADERS="$(curl -I -s http://localhost:3000/)"
|
||||
- STATUS="$(echo $HEADERS | head -n1)"
|
||||
- if [[ "$STATUS" != *"200 OK"* ]]; then echo "$HEADERS"; exit 1; fi
|
@ -1,5 +1,7 @@
|
||||
# Invidious
|
||||
|
||||
[](https://travis-ci.org/omarroth/invidious)
|
||||
|
||||
## Invidious is an alternative front-end to YouTube
|
||||
|
||||
- Audio-only mode (and no need to keep window open on mobile)
|
||||
|
@ -1,15 +1,28 @@
|
||||
FROM archlinux/base
|
||||
|
||||
RUN pacman -Sy --noconfirm shards crystal imagemagick librsvg \
|
||||
which pkgconf gcc ttf-liberation glibc
|
||||
# base-devel contains many other basic packages, that are normally assumed to already exist on a clean arch system
|
||||
|
||||
ADD . /invidious
|
||||
|
||||
FROM alpine:edge AS builder
|
||||
RUN apk add -u crystal shards libc-dev \
|
||||
yaml-dev libxml2-dev sqlite-dev sqlite-static zlib-dev openssl-dev
|
||||
WORKDIR /invidious
|
||||
COPY ./shard.yml ./shard.yml
|
||||
RUN shards update && shards install
|
||||
COPY ./src/ ./src/
|
||||
# TODO: .git folder is required for building – this is destructive.
|
||||
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
||||
COPY ./.git/ ./.git/
|
||||
RUN crystal build --static --release \
|
||||
# TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946
|
||||
-Dmusl \
|
||||
./src/invidious.cr
|
||||
|
||||
RUN sed -i 's/host: localhost/host: postgres/' config/config.yml && \
|
||||
shards update && shards install && \
|
||||
crystal build src/invidious.cr
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk add -u imagemagick ttf-opensans
|
||||
WORKDIR /invidious
|
||||
RUN addgroup -g 1000 -S invidious && \
|
||||
adduser -u 1000 -S invidious -G invidious
|
||||
COPY ./assets/ ./assets/
|
||||
COPY ./config/config.yml ./config/config.yml
|
||||
COPY ./config/sql/ ./config/sql/
|
||||
COPY ./locales/ ./locales/
|
||||
RUN sed -i 's/host: localhost/host: postgres/' config/config.yml
|
||||
COPY --from=builder /invidious/invidious .
|
||||
USER invidious
|
||||
CMD [ "/invidious/invidious" ]
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟",
|
||||
"Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟",
|
||||
"Visual preferences": "التفضيلات المرئية",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "الوضع الليلى: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "الوضع الخفيف: ",
|
||||
"Subscription preferences": "تفضيلات الإشتراك",
|
||||
"Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Ähnliche Videos anzeigen? ",
|
||||
"Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ",
|
||||
"Visual preferences": "Anzeigeeinstellungen",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Nachtmodus: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Schlanker Modus: ",
|
||||
"Subscription preferences": "Abonnementeinstellungen",
|
||||
"Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ",
|
||||
|
@ -74,7 +74,11 @@
|
||||
"Show related videos: ": "Προβολή σχετικών βίντεο; ",
|
||||
"Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :",
|
||||
"Visual preferences": "Προτιμήσεις εμφάνισης",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Σκοτεινή λειτουργία: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Ελαφριά λειτουργία: ",
|
||||
"Subscription preferences": "Προτιμήσεις συνδρομών",
|
||||
"Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ",
|
||||
|
@ -74,7 +74,11 @@
|
||||
"Show related videos: ": "Show related videos: ",
|
||||
"Show annotations by default: ": "Show annotations by default: ",
|
||||
"Visual preferences": "Visual preferences",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Dark mode: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Thin mode: ",
|
||||
"Subscription preferences": "Subscription preferences",
|
||||
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Ĉu montri rilatajn videojn? ",
|
||||
"Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ",
|
||||
"Visual preferences": "Vidaj preferoj",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Malhela reĝimo: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Maldika reĝimo: ",
|
||||
"Subscription preferences": "Abonaj agordoj",
|
||||
"Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "¿Mostrar vídeos relacionados? ",
|
||||
"Show annotations by default: ": "¿Mostrar anotaciones por defecto? ",
|
||||
"Visual preferences": "Preferencias visuales",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Modo oscuro: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Modo compacto: ",
|
||||
"Subscription preferences": "Preferencias de la suscripción",
|
||||
"Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "",
|
||||
"Show annotations by default: ": "",
|
||||
"Visual preferences": "",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "",
|
||||
"Subscription preferences": "",
|
||||
"Show annotations by default for subscribed channels: ": "",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Voir les vidéos liées : ",
|
||||
"Show annotations by default: ": "Voir les annotations par défaut : ",
|
||||
"Visual preferences": "Préférences du site",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Mode Sombre : ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Mode Simplifié : ",
|
||||
"Subscription preferences": "Préférences de la page d'abonnements",
|
||||
"Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies : ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Sýna tengd myndbönd? ",
|
||||
"Show annotations by default: ": "Á að sýna glósur sjálfgefið? ",
|
||||
"Visual preferences": "Sjónrænar stillingar",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Myrkur ham: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Þunnt ham: ",
|
||||
"Subscription preferences": "Áskriftarstillingar",
|
||||
"Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Mostra video correlati? ",
|
||||
"Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ",
|
||||
"Visual preferences": "Preferenze grafiche",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Tema scuro: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Modalità per connessioni lente: ",
|
||||
"Subscription preferences": "Preferenze iscrizioni",
|
||||
"Show annotations by default for subscribed channels: ": "",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Vis relaterte videoer? ",
|
||||
"Show annotations by default: ": "Vis merknader som forvalg? ",
|
||||
"Visual preferences": "Visuelle innstillinger",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Mørk drakt: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Tynt modus: ",
|
||||
"Subscription preferences": "Abonnementsinnstillinger",
|
||||
"Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Gerelateerde video's tonen? ",
|
||||
"Show annotations by default: ": "Standaard annotaties tonen? ",
|
||||
"Visual preferences": "Visuele instellingen",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Donkere modus: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Smalle modus: ",
|
||||
"Subscription preferences": "Abonnementsinstellingen",
|
||||
"Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Pokaż powiązane filmy? ",
|
||||
"Show annotations by default: ": "",
|
||||
"Visual preferences": "Preferencje Wizualne",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Ciemny motyw: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Tryb minimalny: ",
|
||||
"Subscription preferences": "Preferencje subskrybcji",
|
||||
"Show annotations by default for subscribed channels: ": "",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Показывать похожие видео? ",
|
||||
"Show annotations by default: ": "Всегда показывать аннотации? ",
|
||||
"Visual preferences": "Настройки сайта",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Тёмное оформление: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Облегчённое оформление: ",
|
||||
"Subscription preferences": "Настройки подписок",
|
||||
"Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "Показувати схожі відео? ",
|
||||
"Show annotations by default: ": "Завжди показувати анотації? ",
|
||||
"Visual preferences": "Налаштування сайту",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "Темне оформлення: ",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "Полегшене оформлення: ",
|
||||
"Subscription preferences": "Налаштування підписок",
|
||||
"Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ",
|
||||
|
@ -68,7 +68,11 @@
|
||||
"Show related videos: ": "显示相关视频?",
|
||||
"Show annotations by default: ": "默认显示视频注释?",
|
||||
"Visual preferences": "视觉选项",
|
||||
"Player style: ": "",
|
||||
"Dark mode: ": "暗色模式:",
|
||||
"Theme: ": "",
|
||||
"dark": "",
|
||||
"light": "",
|
||||
"Thin mode: ": "窄页模式:",
|
||||
"Subscription preferences": "订阅设置",
|
||||
"Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?",
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: invidious
|
||||
version: 0.19.0
|
||||
version: 0.19.1
|
||||
|
||||
authors:
|
||||
- Omar Roth <omarroth@protonmail.com>
|
||||
@ -11,11 +11,14 @@ targets:
|
||||
dependencies:
|
||||
pg:
|
||||
github: will/crystal-pg
|
||||
version: ~> 0.18.1
|
||||
sqlite3:
|
||||
github: crystal-lang/crystal-sqlite3
|
||||
version: ~> 0.13.0
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
version: ~> 0.26.0
|
||||
|
||||
crystal: 0.29.0
|
||||
crystal: 0.30.1
|
||||
|
||||
license: AGPLv3
|
||||
|
@ -1025,8 +1025,10 @@ get "/api/v1/playlists/:plid" do |env|
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "type", "playlist"
|
||||
json.field "title", playlist.title
|
||||
json.field "playlistId", playlist.id
|
||||
json.field "playlistThumbnail", playlist.thumbnail
|
||||
|
||||
json.field "author", playlist.author
|
||||
json.field "authorId", playlist.ucid
|
||||
@ -1216,7 +1218,7 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||
end
|
||||
|
||||
audio_streams = video.audio_streams(adaptive_fmts)
|
||||
video_streams = video.video_streams(adaptive_fmts)
|
||||
video_streams = video.video_streams(adaptive_fmts).sort_by { |stream| stream["fps"].to_i }.reverse
|
||||
|
||||
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
|
||||
@ -1772,6 +1774,43 @@ get "/sb/:id/:storyboard/:index" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
get "/s_p/:id/:name" do |env|
|
||||
id = env.params.url["id"]
|
||||
name = env.params.url["name"]
|
||||
|
||||
host = "https://i9.ytimg.com"
|
||||
client = make_client(URI.parse(host))
|
||||
url = env.request.resource
|
||||
|
||||
headers = HTTP::Headers.new
|
||||
REQUEST_HEADERS_WHITELIST.each do |header|
|
||||
if env.request.headers[header]?
|
||||
headers[header] = env.request.headers[header]
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
client.get(url, headers) do |response|
|
||||
env.response.status_code = response.status_code
|
||||
response.headers.each do |key, value|
|
||||
if !RESPONSE_HEADERS_BLACKLIST.includes? key
|
||||
env.response.headers[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
if response.status_code >= 300 && response.status_code != 404
|
||||
env.response.headers.delete("Transfer-Encoding")
|
||||
break
|
||||
end
|
||||
|
||||
proxy_file(response, env)
|
||||
end
|
||||
rescue ex
|
||||
end
|
||||
end
|
||||
|
||||
get "/vi/:id/:name" do |env|
|
||||
id = env.params.url["id"]
|
||||
name = env.params.url["name"]
|
||||
|
@ -387,14 +387,15 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
||||
|
||||
html = XML.parse_html(json["content_html"].as_s)
|
||||
nodeset = html.xpath_nodes(%q(//li[contains(@class, "feed-item-container")]))
|
||||
else
|
||||
url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list"
|
||||
elsif auto_generated
|
||||
url = "/channel/#{ucid}"
|
||||
|
||||
if auto_generated
|
||||
url += "&view=50"
|
||||
else
|
||||
url += "&view=1"
|
||||
end
|
||||
response = client.get(url)
|
||||
html = XML.parse_html(response.body)
|
||||
|
||||
nodeset = html.xpath_nodes(%q(//ul[@id="browse-items-primary"]/li[contains(@class, "feed-item-container")]))
|
||||
else
|
||||
url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list&view=1"
|
||||
|
||||
case sort_by
|
||||
when "last", "last_added"
|
||||
|
@ -69,20 +69,20 @@ class FilteredCompressHandler < Kemal::Handler
|
||||
return call_next env if exclude_match? env
|
||||
|
||||
{% if flag?(:without_zlib) %}
|
||||
call_next env
|
||||
{% else %}
|
||||
request_headers = env.request.headers
|
||||
call_next env
|
||||
{% else %}
|
||||
request_headers = env.request.headers
|
||||
|
||||
if request_headers.includes_word?("Accept-Encoding", "gzip")
|
||||
env.response.headers["Content-Encoding"] = "gzip"
|
||||
env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
|
||||
elsif request_headers.includes_word?("Accept-Encoding", "deflate")
|
||||
env.response.headers["Content-Encoding"] = "deflate"
|
||||
env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
|
||||
end
|
||||
if request_headers.includes_word?("Accept-Encoding", "gzip")
|
||||
env.response.headers["Content-Encoding"] = "gzip"
|
||||
env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
|
||||
elsif request_headers.includes_word?("Accept-Encoding", "deflate")
|
||||
env.response.headers["Content-Encoding"] = "deflate"
|
||||
env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
|
||||
end
|
||||
|
||||
call_next env
|
||||
{% end %}
|
||||
call_next env
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -24,6 +24,27 @@ end
|
||||
|
||||
struct ConfigPreferences
|
||||
module StringToArray
|
||||
def self.to_json(value : Array(String), json : JSON::Builder)
|
||||
json.array do
|
||||
value.each do |element|
|
||||
json.string element
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_json(value : JSON::PullParser) : Array(String)
|
||||
begin
|
||||
result = [] of String
|
||||
value.read_array do
|
||||
result << HTML.escape(value.read_string[0, 100])
|
||||
end
|
||||
rescue ex
|
||||
result = [HTML.escape(value.read_string[0, 100]), ""]
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder)
|
||||
yaml.sequence do
|
||||
value.each do |element|
|
||||
@ -44,11 +65,11 @@ struct ConfigPreferences
|
||||
node.raise "Expected scalar, not #{item.class}"
|
||||
end
|
||||
|
||||
result << item.value
|
||||
result << HTML.escape(item.value[0, 100])
|
||||
end
|
||||
rescue ex
|
||||
if node.is_a?(YAML::Nodes::Scalar)
|
||||
result = [node.value, ""]
|
||||
result = [HTML.escape(node.value[0, 100]), ""]
|
||||
else
|
||||
result = ["", ""]
|
||||
end
|
||||
@ -58,6 +79,53 @@ struct ConfigPreferences
|
||||
end
|
||||
end
|
||||
|
||||
module BoolToString
|
||||
def self.to_json(value : String, json : JSON::Builder)
|
||||
json.string value
|
||||
end
|
||||
|
||||
def self.from_json(value : JSON::PullParser) : String
|
||||
begin
|
||||
result = value.read_string
|
||||
|
||||
if result.empty?
|
||||
CONFIG.default_user_preferences.dark_mode
|
||||
else
|
||||
result
|
||||
end
|
||||
rescue ex
|
||||
result = value.read_bool
|
||||
|
||||
if result
|
||||
"dark"
|
||||
else
|
||||
"light"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.to_yaml(value : String, yaml : YAML::Nodes::Builder)
|
||||
yaml.scalar value
|
||||
end
|
||||
|
||||
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
|
||||
unless node.is_a?(YAML::Nodes::Scalar)
|
||||
node.raise "Expected sequence, not #{node.class}"
|
||||
end
|
||||
|
||||
case node.value
|
||||
when "true"
|
||||
"dark"
|
||||
when "false"
|
||||
"light"
|
||||
when ""
|
||||
CONFIG.default_user_preferences.dark_mode
|
||||
else
|
||||
node.value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
yaml_mapping({
|
||||
annotations: {type: Bool, default: false},
|
||||
annotations_subscribed: {type: Bool, default: false},
|
||||
@ -66,13 +134,14 @@ struct ConfigPreferences
|
||||
comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray},
|
||||
continue: {type: Bool, default: false},
|
||||
continue_autoplay: {type: Bool, default: true},
|
||||
dark_mode: {type: Bool, default: false},
|
||||
dark_mode: {type: String, default: "", converter: BoolToString},
|
||||
latest_only: {type: Bool, default: false},
|
||||
listen: {type: Bool, default: false},
|
||||
local: {type: Bool, default: false},
|
||||
locale: {type: String, default: "en-US"},
|
||||
max_results: {type: Int32, default: 40},
|
||||
notifications_only: {type: Bool, default: false},
|
||||
player_style: {type: String, default: "invidious"},
|
||||
quality: {type: String, default: "hd720"},
|
||||
redirect_feed: {type: Bool, default: false},
|
||||
related_videos: {type: Bool, default: true},
|
||||
@ -243,8 +312,7 @@ end
|
||||
|
||||
def extract_videos(nodeset, ucid = nil, author_name = nil)
|
||||
videos = extract_items(nodeset, ucid, author_name)
|
||||
videos.select! { |item| !item.is_a?(SearchChannel | SearchPlaylist) }
|
||||
videos.map { |video| video.as(SearchVideo) }
|
||||
videos.select { |item| item.is_a?(SearchVideo) }.map { |video| video.as(SearchVideo) }
|
||||
end
|
||||
|
||||
def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||
@ -263,18 +331,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||
next
|
||||
end
|
||||
|
||||
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a))
|
||||
if anchor
|
||||
author = anchor.content.strip
|
||||
author_id = anchor["href"].split("/")[-1]
|
||||
end
|
||||
|
||||
author ||= author_name
|
||||
author_id ||= ucid
|
||||
|
||||
author ||= ""
|
||||
author_id ||= ""
|
||||
|
||||
author_id = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.["href"].split("/")[-1] || ucid || ""
|
||||
author = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.content.strip || author_name || ""
|
||||
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")])).try &.to_s || ""
|
||||
|
||||
tile = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-tile")]))
|
||||
@ -292,14 +350,14 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||
anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a))
|
||||
end
|
||||
|
||||
video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b))
|
||||
video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) ||
|
||||
node.xpath_node(%q(.//span[@class="formatted-video-count-label"]))
|
||||
if video_count
|
||||
video_count = video_count.content
|
||||
|
||||
if video_count == "50+"
|
||||
author = "YouTube"
|
||||
author_id = "UC-9-kyTW8ZkZNDHQJ6FgpwQ"
|
||||
video_count = video_count.rchop("+")
|
||||
end
|
||||
|
||||
video_count = video_count.gsub(/\D/, "").to_i?
|
||||
@ -329,22 +387,17 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||
)
|
||||
end
|
||||
|
||||
playlist_thumbnail = node.xpath_node(%q(.//div/span/img)).try &.["data-thumb"]?
|
||||
playlist_thumbnail ||= node.xpath_node(%q(.//div/span/img)).try &.["src"]
|
||||
if !playlist_thumbnail || playlist_thumbnail.empty?
|
||||
thumbnail_id = videos[0]?.try &.id
|
||||
else
|
||||
thumbnail_id = playlist_thumbnail.match(/\/vi\/(?<video_id>[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"]
|
||||
end
|
||||
playlist_thumbnail = node.xpath_node(%q(.//span/img)).try &.["data-thumb"]?
|
||||
playlist_thumbnail ||= node.xpath_node(%q(.//span/img)).try &.["src"]
|
||||
|
||||
items << SearchPlaylist.new(
|
||||
title,
|
||||
plid,
|
||||
author,
|
||||
author_id,
|
||||
video_count,
|
||||
videos,
|
||||
thumbnail_id
|
||||
title: title,
|
||||
id: plid,
|
||||
author: author,
|
||||
ucid: author_id,
|
||||
video_count: video_count,
|
||||
videos: videos,
|
||||
thumbnail: playlist_thumbnail
|
||||
)
|
||||
when .includes? "yt-lockup-channel"
|
||||
author = title.strip
|
||||
@ -379,47 +432,20 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||
else
|
||||
id = id.lchop("/watch?v=")
|
||||
|
||||
metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li))
|
||||
metadata = node.xpath_node(%q(.//div[contains(@class,"yt-lockup-meta")]/ul))
|
||||
|
||||
begin
|
||||
published = decode_date(metadata[0].content.lchop("Streamed ").lchop("Starts "))
|
||||
rescue ex
|
||||
end
|
||||
begin
|
||||
published ||= Time.unix(metadata[0].xpath_node(%q(.//span)).not_nil!["data-timestamp"].to_i64)
|
||||
rescue ex
|
||||
end
|
||||
published = metadata.try &.xpath_node(%q(.//li[contains(text(), " ago")])).try { |node| decode_date(node.content.sub(/^[a-zA-Z]+ /, "")) }
|
||||
published ||= metadata.try &.xpath_node(%q(.//span[@data-timestamp])).try { |node| Time.unix(node["data-timestamp"].to_i64) }
|
||||
published ||= Time.utc
|
||||
|
||||
begin
|
||||
view_count = metadata[0].content.rchop(" watching").delete(",").try &.to_i64?
|
||||
rescue ex
|
||||
end
|
||||
begin
|
||||
view_count ||= metadata.try &.[1].content.delete("No views,").try &.to_i64?
|
||||
rescue ex
|
||||
end
|
||||
view_count = metadata.try &.xpath_node(%q(.//li[contains(text(), " views")])).try &.content.gsub(/\D/, "").to_i64?
|
||||
view_count ||= 0_i64
|
||||
|
||||
length_seconds = node.xpath_node(%q(.//span[@class="video-time"]))
|
||||
if length_seconds
|
||||
length_seconds = decode_length_seconds(length_seconds.content)
|
||||
else
|
||||
length_seconds = -1
|
||||
end
|
||||
length_seconds = node.xpath_node(%q(.//span[@class="video-time"])).try { |node| decode_length_seconds(node.content) }
|
||||
length_seconds ||= -1
|
||||
|
||||
live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")]))
|
||||
if live_now
|
||||
live_now = true
|
||||
else
|
||||
live_now = false
|
||||
end
|
||||
|
||||
if node.xpath_node(%q(.//span[text()="Premium"]))
|
||||
premium = true
|
||||
else
|
||||
premium = false
|
||||
end
|
||||
live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")])) ? true : false
|
||||
premium = node.xpath_node(%q(.//span[text()="Premium"])) ? true : false
|
||||
|
||||
if !premium || node.xpath_node(%q(.//span[contains(text(), "Free episode")]))
|
||||
paid = false
|
||||
@ -457,26 +483,18 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
|
||||
|
||||
nodeset.each do |shelf|
|
||||
shelf_anchor = shelf.xpath_node(%q(.//h2[contains(@class, "branded-page-module-title")]))
|
||||
next if !shelf_anchor
|
||||
|
||||
if !shelf_anchor
|
||||
next
|
||||
end
|
||||
|
||||
title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")]))
|
||||
if title
|
||||
title = title.content.strip
|
||||
end
|
||||
title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")])).try &.content.strip
|
||||
title ||= ""
|
||||
|
||||
id = shelf_anchor.xpath_node(%q(.//a)).try &.["href"]
|
||||
if !id
|
||||
next
|
||||
end
|
||||
next if !id
|
||||
|
||||
is_playlist = false
|
||||
shelf_is_playlist = false
|
||||
videos = [] of SearchPlaylistVideo
|
||||
|
||||
shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list")]/li)).each do |child_node|
|
||||
shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list") or contains(@class, "expanded-shelf-content-list")]/li)).each do |child_node|
|
||||
type = child_node.xpath_node(%q(./div))
|
||||
if !type
|
||||
next
|
||||
@ -484,7 +502,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
|
||||
|
||||
case type["class"]
|
||||
when .includes? "yt-lockup-video"
|
||||
is_playlist = true
|
||||
shelf_is_playlist = true
|
||||
|
||||
anchor = child_node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
|
||||
if anchor
|
||||
@ -517,41 +535,60 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
|
||||
|
||||
playlist_thumbnail = child_node.xpath_node(%q(.//span/img)).try &.["data-thumb"]?
|
||||
playlist_thumbnail ||= child_node.xpath_node(%q(.//span/img)).try &.["src"]
|
||||
if !playlist_thumbnail || playlist_thumbnail.empty?
|
||||
thumbnail_id = videos[0]?.try &.id
|
||||
else
|
||||
thumbnail_id = playlist_thumbnail.match(/\/vi\/(?<video_id>[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"]
|
||||
end
|
||||
|
||||
video_count_label = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]))
|
||||
if video_count_label
|
||||
video_count = video_count_label.content.gsub(/\D/, "").to_i?
|
||||
video_count = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) ||
|
||||
child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]))
|
||||
if video_count
|
||||
video_count = video_count.content.gsub(/\D/, "").to_i?
|
||||
end
|
||||
video_count ||= 50
|
||||
|
||||
videos = [] of SearchPlaylistVideo
|
||||
child_node.xpath_nodes(%q(.//*[contains(@class, "yt-lockup-playlist-items")]/li)).each do |video|
|
||||
anchor = video.xpath_node(%q(.//a))
|
||||
if anchor
|
||||
video_title = anchor.content.strip
|
||||
id = HTTP::Params.parse(URI.parse(anchor["href"]).query.not_nil!)["v"]
|
||||
end
|
||||
video_title ||= ""
|
||||
id ||= ""
|
||||
|
||||
anchor = video.xpath_node(%q(.//span/span))
|
||||
if anchor
|
||||
length_seconds = decode_length_seconds(anchor.content)
|
||||
end
|
||||
length_seconds ||= 0
|
||||
|
||||
videos << SearchPlaylistVideo.new(
|
||||
video_title,
|
||||
id,
|
||||
length_seconds
|
||||
)
|
||||
end
|
||||
|
||||
items << SearchPlaylist.new(
|
||||
playlist_title,
|
||||
plid,
|
||||
author_name,
|
||||
ucid,
|
||||
video_count,
|
||||
Array(SearchPlaylistVideo).new,
|
||||
thumbnail_id
|
||||
title: playlist_title,
|
||||
id: plid,
|
||||
author: author_name,
|
||||
ucid: ucid,
|
||||
video_count: video_count,
|
||||
videos: videos,
|
||||
thumbnail: playlist_thumbnail
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if is_playlist
|
||||
if shelf_is_playlist
|
||||
plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"]
|
||||
|
||||
items << SearchPlaylist.new(
|
||||
title,
|
||||
plid,
|
||||
author_name,
|
||||
ucid,
|
||||
videos.size,
|
||||
videos,
|
||||
videos[0].try &.id
|
||||
title: title,
|
||||
id: plid,
|
||||
author: author_name,
|
||||
ucid: ucid,
|
||||
video_count: videos.size,
|
||||
videos: videos,
|
||||
thumbnail: "https://i.ytimg.com/vi/#{videos[0].id}/mqdefault.jpg"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -4,7 +4,7 @@ def Object.from_json(string_or_io, default) : self
|
||||
new parser, default
|
||||
end
|
||||
|
||||
# Adds configurable 'default' to
|
||||
# Adds configurable 'default'
|
||||
macro patched_json_mapping(_properties_, strict = false)
|
||||
{% for key, value in _properties_ %}
|
||||
{% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %}
|
||||
|
@ -31,10 +31,10 @@ class HTTPProxy
|
||||
|
||||
if resp[:code]? == 200
|
||||
{% if !flag?(:without_openssl) %}
|
||||
if tls
|
||||
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
||||
socket = tls_socket
|
||||
end
|
||||
if tls
|
||||
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
||||
socket = tls_socket
|
||||
end
|
||||
{% end %}
|
||||
|
||||
return socket
|
||||
|
@ -356,3 +356,16 @@ def parse_range(range)
|
||||
|
||||
return 0_i64, nil
|
||||
end
|
||||
|
||||
def convert_theme(theme)
|
||||
case theme
|
||||
when "true"
|
||||
"dark"
|
||||
when "false"
|
||||
"light"
|
||||
when "", nil
|
||||
nil
|
||||
else
|
||||
theme
|
||||
end
|
||||
end
|
||||
|
@ -51,6 +51,7 @@ struct Playlist
|
||||
video_count: Int32,
|
||||
views: Int64,
|
||||
updated: Time,
|
||||
thumbnail: String?,
|
||||
})
|
||||
end
|
||||
|
||||
@ -223,6 +224,9 @@ def fetch_playlist(plid, locale)
|
||||
description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1])).try &.to_s ||
|
||||
document.xpath_node(%q(//span[@class="pl-header-description-text"])).try &.to_s || ""
|
||||
|
||||
playlist_thumbnail = document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["data-thumb"]? ||
|
||||
document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["src"]
|
||||
|
||||
# YouTube allows anonymous playlists, so most of this can be empty or optional
|
||||
anchor = document.xpath_node(%q(//ul[@class="pl-header-details"]))
|
||||
author = anchor.try &.xpath_node(%q(.//li[1]/a)).try &.content
|
||||
@ -234,15 +238,12 @@ def fetch_playlist(plid, locale)
|
||||
|
||||
video_count = anchor.try &.xpath_node(%q(.//li[2])).try &.content.gsub(/\D/, "").to_i?
|
||||
video_count ||= 0
|
||||
views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.delete("No views, ").to_i64?
|
||||
|
||||
views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.gsub(/\D/, "").to_i64?
|
||||
views ||= 0_i64
|
||||
|
||||
updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ")
|
||||
if updated
|
||||
updated = decode_date(updated)
|
||||
else
|
||||
updated = Time.utc
|
||||
end
|
||||
updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ").try { |date| decode_date(date) }
|
||||
updated ||= Time.utc
|
||||
|
||||
playlist = Playlist.new(
|
||||
title: title,
|
||||
@ -253,7 +254,8 @@ def fetch_playlist(plid, locale)
|
||||
description_html: description_html,
|
||||
video_count: video_count,
|
||||
views: views,
|
||||
updated: updated
|
||||
updated: updated,
|
||||
thumbnail: playlist_thumbnail,
|
||||
)
|
||||
|
||||
return playlist
|
||||
|
@ -117,6 +117,7 @@ struct SearchPlaylist
|
||||
json.field "type", "playlist"
|
||||
json.field "title", self.title
|
||||
json.field "playlistId", self.id
|
||||
json.field "playlistThumbnail", self.thumbnail
|
||||
|
||||
json.field "author", self.author
|
||||
json.field "authorId", self.ucid
|
||||
@ -152,13 +153,13 @@ struct SearchPlaylist
|
||||
end
|
||||
|
||||
db_mapping({
|
||||
title: String,
|
||||
id: String,
|
||||
author: String,
|
||||
ucid: String,
|
||||
video_count: Int32,
|
||||
videos: Array(SearchPlaylistVideo),
|
||||
thumbnail_id: String?,
|
||||
title: String,
|
||||
id: String,
|
||||
author: String,
|
||||
ucid: String,
|
||||
video_count: Int32,
|
||||
videos: Array(SearchPlaylistVideo),
|
||||
thumbnail: String?,
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -31,62 +31,6 @@ struct User
|
||||
end
|
||||
|
||||
struct Preferences
|
||||
module StringToArray
|
||||
def self.to_json(value : Array(String), json : JSON::Builder)
|
||||
json.array do
|
||||
value.each do |element|
|
||||
json.string element
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_json(value : JSON::PullParser) : Array(String)
|
||||
begin
|
||||
result = [] of String
|
||||
value.read_array do
|
||||
result << HTML.escape(value.read_string[0, 100])
|
||||
end
|
||||
rescue ex
|
||||
result = [HTML.escape(value.read_string[0, 100]), ""]
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder)
|
||||
yaml.sequence do
|
||||
value.each do |element|
|
||||
yaml.scalar element
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String)
|
||||
begin
|
||||
unless node.is_a?(YAML::Nodes::Sequence)
|
||||
node.raise "Expected sequence, not #{node.class}"
|
||||
end
|
||||
|
||||
result = [] of String
|
||||
node.nodes.each do |item|
|
||||
unless item.is_a?(YAML::Nodes::Scalar)
|
||||
node.raise "Expected scalar, not #{item.class}"
|
||||
end
|
||||
|
||||
result << HTML.escape(item.value[0, 100])
|
||||
end
|
||||
rescue ex
|
||||
if node.is_a?(YAML::Nodes::Scalar)
|
||||
result = [HTML.escape(node.value[0, 100]), ""]
|
||||
else
|
||||
result = ["", ""]
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
module ProcessString
|
||||
def self.to_json(value : String, json : JSON::Builder)
|
||||
json.string value
|
||||
@ -127,17 +71,18 @@ struct Preferences
|
||||
annotations: {type: Bool, default: CONFIG.default_user_preferences.annotations},
|
||||
annotations_subscribed: {type: Bool, default: CONFIG.default_user_preferences.annotations_subscribed},
|
||||
autoplay: {type: Bool, default: CONFIG.default_user_preferences.autoplay},
|
||||
captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: StringToArray},
|
||||
comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: StringToArray},
|
||||
captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: ConfigPreferences::StringToArray},
|
||||
comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: ConfigPreferences::StringToArray},
|
||||
continue: {type: Bool, default: CONFIG.default_user_preferences.continue},
|
||||
continue_autoplay: {type: Bool, default: CONFIG.default_user_preferences.continue_autoplay},
|
||||
dark_mode: {type: Bool, default: CONFIG.default_user_preferences.dark_mode},
|
||||
dark_mode: {type: String, default: CONFIG.default_user_preferences.dark_mode, converter: ConfigPreferences::BoolToString},
|
||||
latest_only: {type: Bool, default: CONFIG.default_user_preferences.latest_only},
|
||||
listen: {type: Bool, default: CONFIG.default_user_preferences.listen},
|
||||
local: {type: Bool, default: CONFIG.default_user_preferences.local},
|
||||
locale: {type: String, default: CONFIG.default_user_preferences.locale, converter: ProcessString},
|
||||
max_results: {type: Int32, default: CONFIG.default_user_preferences.max_results, converter: ClampInt},
|
||||
notifications_only: {type: Bool, default: CONFIG.default_user_preferences.notifications_only},
|
||||
player_style: {type: String, default: CONFIG.default_user_preferences.player_style, converter: ProcessString},
|
||||
quality: {type: String, default: CONFIG.default_user_preferences.quality, converter: ProcessString},
|
||||
redirect_feed: {type: Bool, default: CONFIG.default_user_preferences.redirect_feed},
|
||||
related_videos: {type: Bool, default: CONFIG.default_user_preferences.related_videos},
|
||||
|
@ -108,33 +108,7 @@ CAPTION_LANGUAGES = {
|
||||
"Zulu",
|
||||
}
|
||||
|
||||
REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"}
|
||||
BYPASS_REGIONS = {
|
||||
"GB",
|
||||
"DE",
|
||||
"FR",
|
||||
"IN",
|
||||
"CN",
|
||||
"RU",
|
||||
"CA",
|
||||
"JP",
|
||||
"IT",
|
||||
"TH",
|
||||
"ES",
|
||||
"AE",
|
||||
"KR",
|
||||
"IR",
|
||||
"BR",
|
||||
"PK",
|
||||
"ID",
|
||||
"BD",
|
||||
"MX",
|
||||
"PH",
|
||||
"EG",
|
||||
"VN",
|
||||
"CD",
|
||||
"TR",
|
||||
}
|
||||
REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"}
|
||||
|
||||
# See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476
|
||||
VIDEO_FORMATS = {
|
||||
@ -258,6 +232,7 @@ struct VideoPreferences
|
||||
listen: Bool,
|
||||
local: Bool,
|
||||
preferred_captions: Array(String),
|
||||
player_style: String,
|
||||
quality: String,
|
||||
raw: Bool,
|
||||
region: String?,
|
||||
@ -803,8 +778,11 @@ struct Video
|
||||
end
|
||||
|
||||
def premium
|
||||
premium = self.player_response.to_s.includes? "Get YouTube without the ads."
|
||||
return premium
|
||||
if info["premium"]?
|
||||
self.info["premium"] == "true"
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def captions
|
||||
@ -1128,34 +1106,24 @@ def fetch_video(id, region)
|
||||
info = extract_player_config(response.body, html)
|
||||
info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
|
||||
|
||||
# Try to use proxies for region-blocked videos
|
||||
if info["reason"]? && info["reason"].includes? "your country"
|
||||
bypass_channel = Channel({XML::Node, HTTP::Params} | Nil).new
|
||||
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",")
|
||||
if !allowed_regions || allowed_regions == [""]
|
||||
allowed_regions = [] of String
|
||||
end
|
||||
|
||||
PROXY_LIST.each do |proxy_region, list|
|
||||
spawn do
|
||||
client = make_client(YT_URL, proxy_region)
|
||||
proxy_response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
|
||||
# Check for region-blocks
|
||||
if info["reason"]? && info["reason"].includes?("your country")
|
||||
bypass_regions = PROXY_LIST.keys & allowed_regions
|
||||
if !bypass_regions.empty?
|
||||
region = bypass_regions[rand(bypass_regions.size)]
|
||||
client = make_client(YT_URL, region)
|
||||
response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
|
||||
|
||||
proxy_html = XML.parse_html(proxy_response.body)
|
||||
proxy_info = extract_player_config(proxy_response.body, proxy_html)
|
||||
html = XML.parse_html(response.body)
|
||||
info = extract_player_config(response.body, html)
|
||||
|
||||
if !proxy_info["reason"]?
|
||||
proxy_info["region"] = proxy_region
|
||||
proxy_info["cookie"] = proxy_response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
|
||||
bypass_channel.send({proxy_html, proxy_info})
|
||||
else
|
||||
bypass_channel.send(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
PROXY_LIST.size.times do
|
||||
response = bypass_channel.receive
|
||||
if response
|
||||
html, info = response
|
||||
break
|
||||
end
|
||||
info["region"] = region if region
|
||||
info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
|
||||
end
|
||||
end
|
||||
|
||||
@ -1175,7 +1143,7 @@ def fetch_video(id, region)
|
||||
end
|
||||
end
|
||||
|
||||
if info["errorcode"]?.try &.== "2" || !info["player_response"]
|
||||
if !info["player_response"]? || info["errorcode"]?.try &.== "2"
|
||||
raise "Video unavailable."
|
||||
end
|
||||
|
||||
@ -1189,6 +1157,8 @@ def fetch_video(id, region)
|
||||
author = player_json["videoDetails"]["author"]?.try &.as_s || ""
|
||||
ucid = player_json["videoDetails"]["channelId"]?.try &.as_s || ""
|
||||
|
||||
info["premium"] = html.xpath_node(%q(.//span[text()="Premium"])) ? "true" : "false"
|
||||
|
||||
views = html.xpath_node(%q(//meta[@itemprop="interactionCount"]))
|
||||
.try &.["content"].to_i64? || 0_i64
|
||||
|
||||
@ -1209,9 +1179,6 @@ def fetch_video(id, region)
|
||||
published ||= Time.utc.to_s("%Y-%m-%d")
|
||||
published = Time.parse(published, "%Y-%m-%d", Time::Location.local)
|
||||
|
||||
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",")
|
||||
allowed_regions ||= [] of String
|
||||
|
||||
is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).try &.["content"] == "True"
|
||||
is_family_friendly ||= true
|
||||
|
||||
@ -1259,6 +1226,7 @@ def process_video_params(query, preferences)
|
||||
continue_autoplay = query["continue_autoplay"]?.try &.to_i?
|
||||
listen = query["listen"]? && (query["listen"] == "true" || query["listen"] == "1").to_unsafe
|
||||
local = query["local"]? && (query["local"] == "true" || query["local"] == "1").to_unsafe
|
||||
player_style = query["player_style"]?
|
||||
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
||||
quality = query["quality"]?
|
||||
region = query["region"]?
|
||||
@ -1276,6 +1244,7 @@ def process_video_params(query, preferences)
|
||||
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
|
||||
listen ||= preferences.listen.to_unsafe
|
||||
local ||= preferences.local.to_unsafe
|
||||
player_style ||= preferences.player_style
|
||||
preferred_captions ||= preferences.captions
|
||||
quality ||= preferences.quality
|
||||
related_videos ||= preferences.related_videos.to_unsafe
|
||||
@ -1291,6 +1260,7 @@ def process_video_params(query, preferences)
|
||||
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
|
||||
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
|
||||
local ||= CONFIG.default_user_preferences.local.to_unsafe
|
||||
player_style ||= CONFIG.default_user_preferences.player_style
|
||||
preferred_captions ||= CONFIG.default_user_preferences.captions
|
||||
quality ||= CONFIG.default_user_preferences.quality
|
||||
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
|
||||
@ -1349,6 +1319,7 @@ def process_video_params(query, preferences)
|
||||
controls: controls,
|
||||
listen: listen,
|
||||
local: local,
|
||||
player_style: player_style,
|
||||
preferred_captions: preferred_captions,
|
||||
quality: quality,
|
||||
raw: raw,
|
||||
|
Loading…
Reference in New Issue
Block a user