Compare commits
34 Commits
ce7ac8d6ca
...
86caa5cfae
Author | SHA1 | Date | |
---|---|---|---|
86caa5cfae | |||
|
9e8baa3539 | ||
|
07fe648a9c | ||
|
6da3287e9d | ||
|
37c2f5caed | ||
|
07b366f06b | ||
|
e8a14446af | ||
|
813dc6de1c | ||
|
72478ba704 | ||
|
9e970fe4bd | ||
|
d76fed5850 | ||
|
6868cade05 | ||
|
67571b2492 | ||
|
d5df81f0f8 | ||
|
eb27e097ed | ||
|
9ce9c54399 | ||
|
3a5d408602 | ||
|
7e363fa3c8 | ||
|
d9416a0be5 | ||
|
8338a73e7b | ||
|
86ee761788 | ||
|
c5b87e3b5e | ||
|
ed8b84ed15 | ||
|
8ce91166d6 | ||
|
8525758583 | ||
|
ab4c0a1d3c | ||
|
c31908a011 | ||
|
2562f80695 | ||
|
fead0e14ac | ||
|
438467f69a | ||
|
db3c57d49f | ||
|
07de1e236f | ||
|
27d8fa112d | ||
|
2a092577c6 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@ -38,16 +38,16 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
stable: [true]
|
stable: [true]
|
||||||
crystal:
|
crystal:
|
||||||
- 1.6.2
|
|
||||||
- 1.7.3
|
- 1.7.3
|
||||||
- 1.8.2
|
- 1.8.2
|
||||||
- 1.9.2
|
- 1.9.2
|
||||||
|
- 1.10.1
|
||||||
include:
|
include:
|
||||||
- crystal: nightly
|
- crystal: nightly
|
||||||
stable: false
|
stable: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build Docker
|
- name: Build Docker
|
||||||
run: docker-compose build --build-arg release=0
|
run: docker-compose build --build-arg release=0
|
||||||
@ -103,18 +103,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build Docker ARM64 image
|
- name: Build Docker ARM64 image
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
47
.github/workflows/container-release.yml
vendored
47
.github/workflows/container-release.yml
vendored
@ -11,7 +11,6 @@ on:
|
|||||||
- invidious.service
|
- invidious.service
|
||||||
- .git*
|
- .git*
|
||||||
- .editorconfig
|
- .editorconfig
|
||||||
|
|
||||||
- screenshots/*
|
- screenshots/*
|
||||||
- .github/ISSUE_TEMPLATE/*
|
- .github/ISSUE_TEMPLATE/*
|
||||||
- kubernetes/**
|
- kubernetes/**
|
||||||
@ -22,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
uses: crystal-lang/install-crystal@v1.8.0
|
||||||
@ -38,42 +37,64 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_PASSWORD }}
|
password: ${{ secrets.QUAY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: quay.io/invidious/invidious
|
||||||
|
tags: |
|
||||||
|
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
labels: |
|
||||||
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker AMD64 image for Push Event
|
- name: Build and push Docker AMD64 image for Push Event
|
||||||
if: github.ref == 'refs/heads/master'
|
uses: docker/build-push-action@v5
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
labels: quay.expires-after=12w
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
push: true
|
push: true
|
||||||
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta-arm64
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: quay.io/invidious/invidious
|
||||||
|
flavor: |
|
||||||
|
suffix=-arm64
|
||||||
|
tags: |
|
||||||
|
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
labels: |
|
||||||
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker ARM64 image for Push Event
|
- name: Build and push Docker ARM64 image for Push Event
|
||||||
if: github.ref == 'refs/heads/master'
|
uses: docker/build-push-action@v5
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
platforms: linux/arm64/v8
|
platforms: linux/arm64/v8
|
||||||
labels: quay.expires-after=12w
|
labels: ${{ steps.meta-arm64.outputs.labels }}
|
||||||
push: true
|
push: true
|
||||||
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64
|
tags: ${{ steps.meta-arm64.outputs.tags }}
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v5
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 365
|
days-before-stale: 365
|
||||||
|
13
README.md
13
README.md
@ -145,18 +145,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
|
|||||||
|
|
||||||
## Projects using Invidious
|
## Projects using Invidious
|
||||||
|
|
||||||
- [FreeTube](https://github.com/FreeTubeApp/FreeTube): A libre software YouTube app for privacy.
|
A list of projects and extensions for or utilizing Invidious can be found in the documentation: https://docs.invidious.io/applications/
|
||||||
- [CloudTube](https://sr.ht/~cadence/tube/): A JavaScript-rich alternate YouTube player.
|
|
||||||
- [PeerTubeify](https://gitlab.com/Cha_de_L/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists.
|
|
||||||
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube.
|
|
||||||
- [HoloPlay](https://github.com/stephane-r/holoplay-pwa): Progressive Web App connecting on Invidious API's with search, playlists and favorites.
|
|
||||||
- [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch.
|
|
||||||
- [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV.
|
|
||||||
- [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client.
|
|
||||||
- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API).
|
|
||||||
- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV.
|
|
||||||
- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android.
|
|
||||||
|
|
||||||
|
|
||||||
## Liability
|
## Liability
|
||||||
|
|
||||||
|
@ -747,6 +747,17 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safari screen timeout on looped video playback fix
|
||||||
|
if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) {
|
||||||
|
player.loop(false);
|
||||||
|
player.ready(function () {
|
||||||
|
player.on('ended', function () {
|
||||||
|
player.currentTime(0);
|
||||||
|
player.play();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Watch on Invidious link
|
// Watch on Invidious link
|
||||||
if (location.pathname.startsWith('/embed/')) {
|
if (location.pathname.startsWith('/embed/')) {
|
||||||
const Button = videojs.getComponent('Button');
|
const Button = videojs.getComponent('Button');
|
||||||
|
@ -392,27 +392,6 @@ jobs:
|
|||||||
enable: true
|
enable: true
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Captcha API
|
|
||||||
# -----------------------------
|
|
||||||
|
|
||||||
##
|
|
||||||
## URL of the captcha solving service.
|
|
||||||
##
|
|
||||||
## Accepted values: any URL
|
|
||||||
## Default: https://api.anti-captcha.com
|
|
||||||
##
|
|
||||||
#captcha_api_url: https://api.anti-captcha.com
|
|
||||||
|
|
||||||
##
|
|
||||||
## API key for the captcha solving service.
|
|
||||||
##
|
|
||||||
## Accepted values: a string
|
|
||||||
## Default: <none>
|
|
||||||
##
|
|
||||||
#captcha_key:
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
@ -33,7 +33,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.18
|
||||||
RUN apk add --no-cache librsvg ttf-opensans tini
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
adduser -u 1000 -S invidious -G invidious
|
adduser -u 1000 -S invidious -G invidious
|
||||||
|
@ -33,7 +33,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.18
|
||||||
RUN apk add --no-cache librsvg ttf-opensans tini
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
adduser -u 1000 -S invidious -G invidious
|
adduser -u 1000 -S invidious -G invidious
|
||||||
|
@ -3,18 +3,6 @@ require "../spec_helper"
|
|||||||
CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
|
CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
|
||||||
|
|
||||||
Spectator.describe "Helper" do
|
Spectator.describe "Helper" do
|
||||||
describe "#produce_channel_videos_url" do
|
|
||||||
it "correctly produces url for requesting page `x` of a channel's videos" do
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")).to eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en")
|
|
||||||
#
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en")
|
|
||||||
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20)).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en")
|
|
||||||
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#produce_channel_search_continuation" do
|
describe "#produce_channel_search_continuation" do
|
||||||
it "correctly produces token for searching a specific channel" do
|
it "correctly produces token for searching a specific channel" do
|
||||||
expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
|
expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
|
||||||
|
@ -18,8 +18,8 @@ record AboutChannel,
|
|||||||
|
|
||||||
def get_about_info(ucid, locale) : AboutChannel
|
def get_about_info(ucid, locale) : AboutChannel
|
||||||
begin
|
begin
|
||||||
# "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"}
|
# Fetch channel information from channel home page
|
||||||
initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dA==")
|
initdata = YoutubeAPI.browse(browse_id: ucid, params: "")
|
||||||
rescue
|
rescue
|
||||||
raise InfoException.new("Could not get channel info.")
|
raise InfoException.new("Could not get channel info.")
|
||||||
end
|
end
|
||||||
|
@ -93,7 +93,7 @@ struct ChannelVideo
|
|||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
{
|
{
|
||||||
{{*@type.instance_vars.map(&.name)}}
|
{{@type.instance_vars.map(&.name).splat}}
|
||||||
}
|
}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
@ -62,12 +62,6 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so
|
|||||||
return continuation
|
return continuation
|
||||||
end
|
end
|
||||||
|
|
||||||
# Used in bypass_captcha_job.cr
|
|
||||||
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
|
||||||
continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2)
|
|
||||||
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
|
||||||
end
|
|
||||||
|
|
||||||
module Invidious::Channel::Tabs
|
module Invidious::Channel::Tabs
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ struct ConfigPreferences
|
|||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
{
|
{
|
||||||
{{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}}
|
{{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}}
|
||||||
}
|
}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
@ -84,6 +84,7 @@ class Config
|
|||||||
|
|
||||||
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||||
property https_only : Bool?
|
property https_only : Bool?
|
||||||
|
property login_only : Bool?
|
||||||
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||||
property hmac_key : String = ""
|
property hmac_key : String = ""
|
||||||
# Domain to be used for links to resources on the site where an absolute URL is required
|
# Domain to be used for links to resources on the site where an absolute URL is required
|
||||||
@ -133,10 +134,6 @@ class Config
|
|||||||
# Saved cookies in "name1=value1; name2=value2..." format
|
# Saved cookies in "name1=value1; name2=value2..." format
|
||||||
@[YAML::Field(converter: Preferences::StringToCookies)]
|
@[YAML::Field(converter: Preferences::StringToCookies)]
|
||||||
property cookies : HTTP::Cookies = HTTP::Cookies.new
|
property cookies : HTTP::Cookies = HTTP::Cookies.new
|
||||||
# Key for Anti-Captcha
|
|
||||||
property captcha_key : String? = nil
|
|
||||||
# API URL for Anti-Captcha
|
|
||||||
property captcha_api_url : String = "https://api.anti-captcha.com"
|
|
||||||
|
|
||||||
# Playlist length limit
|
# Playlist length limit
|
||||||
property playlist_length_limit : Int32 = 500
|
property playlist_length_limit : Int32 = 500
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
macro error_template(*args)
|
macro error_template(*args)
|
||||||
error_template_helper(env, {{*args}})
|
error_template_helper(env, {{args.splat}})
|
||||||
end
|
end
|
||||||
|
|
||||||
def github_details(summary : String, content : String)
|
def github_details(summary : String, content : String)
|
||||||
@ -95,7 +95,7 @@ end
|
|||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
macro error_atom(*args)
|
macro error_atom(*args)
|
||||||
error_atom_helper(env, {{*args}})
|
error_atom_helper(env, {{args.splat}})
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
|
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
|
||||||
@ -121,7 +121,7 @@ end
|
|||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
macro error_json(*args)
|
macro error_json(*args)
|
||||||
error_json_helper(env, {{*args}})
|
error_json_helper(env, {{args.splat}})
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_json_helper(
|
def error_json_helper(
|
||||||
|
@ -208,3 +208,20 @@ def proxy_file(response, env)
|
|||||||
IO.copy response.body_io, env.response
|
IO.copy response.body_io, env.response
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fetch the playback requests tracker from the statistics endpoint.
|
||||||
|
#
|
||||||
|
# Creates a new tracker when unavailable.
|
||||||
|
def get_playback_statistic
|
||||||
|
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty?
|
||||||
|
tracker = {
|
||||||
|
"totalRequests" => 0_i64,
|
||||||
|
"successfulRequests" => 0_i64,
|
||||||
|
"ratio" => 0_f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
|
||||||
|
end
|
||||||
|
|
||||||
|
return tracker.as(Hash(String, Int64 | Float64))
|
||||||
|
end
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob
|
|
||||||
def begin
|
|
||||||
loop do
|
|
||||||
begin
|
|
||||||
random_video = PG_DB.query_one?("select id, ucid from (select id, ucid from channel_videos limit 1000) as s ORDER BY RANDOM() LIMIT 1", as: {id: String, ucid: String})
|
|
||||||
if !random_video
|
|
||||||
random_video = {id: "zj82_v2R6ts", ucid: "UCK87Lox575O_HCHBWaBSyGA"}
|
|
||||||
end
|
|
||||||
{"/watch?v=#{random_video["id"]}&gl=US&hl=en&has_verified=1&bpctr=9999999999", produce_channel_videos_url(ucid: random_video["ucid"])}.each do |path|
|
|
||||||
response = YT_POOL.client &.get(path)
|
|
||||||
if response.body.includes?("To continue with your YouTube experience, please fill out the form below.")
|
|
||||||
html = XML.parse_html(response.body)
|
|
||||||
form = html.xpath_node(%(//form[@action="/das_captcha"])).not_nil!
|
|
||||||
site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"]
|
|
||||||
s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"]
|
|
||||||
|
|
||||||
inputs = {} of String => String
|
|
||||||
form.xpath_nodes(%(.//input[@name])).map do |node|
|
|
||||||
inputs[node["name"]] = node["value"]
|
|
||||||
end
|
|
||||||
|
|
||||||
headers = response.cookies.add_request_headers(HTTP::Headers.new)
|
|
||||||
|
|
||||||
response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/createTask",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"task" => {
|
|
||||||
"type" => "NoCaptchaTaskProxyless",
|
|
||||||
"websiteURL" => "https://www.youtube.com#{path}",
|
|
||||||
"websiteKey" => site_key,
|
|
||||||
"recaptchaDataSValue" => s_value,
|
|
||||||
},
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
raise response["error"].as_s if response["error"]?
|
|
||||||
task_id = response["taskId"].as_i
|
|
||||||
|
|
||||||
loop do
|
|
||||||
sleep 10.seconds
|
|
||||||
|
|
||||||
response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/getTaskResult",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"taskId" => task_id,
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
if response["status"]?.try &.== "ready"
|
|
||||||
break
|
|
||||||
elsif response["errorId"]?.try &.as_i != 0
|
|
||||||
raise response["errorDescription"].as_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
|
|
||||||
headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
|
|
||||||
response = YT_POOL.client &.post("/das_captcha", headers, form: inputs)
|
|
||||||
|
|
||||||
response.cookies
|
|
||||||
.select { |cookie| cookie.name != "PREF" }
|
|
||||||
.each { |cookie| CONFIG.cookies << cookie }
|
|
||||||
|
|
||||||
# Persist cookies between runs
|
|
||||||
File.write("config/config.yml", CONFIG.to_yaml)
|
|
||||||
elsif response.headers["Location"]?.try &.includes?("/sorry/index")
|
|
||||||
location = response.headers["Location"].try { |u| URI.parse(u) }
|
|
||||||
headers = HTTP::Headers{":authority" => location.host.not_nil!}
|
|
||||||
response = YT_POOL.client &.get(location.request_target, headers)
|
|
||||||
|
|
||||||
html = XML.parse_html(response.body)
|
|
||||||
form = html.xpath_node(%(//form[@action="index"])).not_nil!
|
|
||||||
site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"]
|
|
||||||
s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"]
|
|
||||||
|
|
||||||
inputs = {} of String => String
|
|
||||||
form.xpath_nodes(%(.//input[@name])).map do |node|
|
|
||||||
inputs[node["name"]] = node["value"]
|
|
||||||
end
|
|
||||||
|
|
||||||
captcha_client = HTTPClient.new(URI.parse(CONFIG.captcha_api_url))
|
|
||||||
captcha_client.family = CONFIG.force_resolve || Socket::Family::INET
|
|
||||||
response = JSON.parse(captcha_client.post("/createTask",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"task" => {
|
|
||||||
"type" => "NoCaptchaTaskProxyless",
|
|
||||||
"websiteURL" => location.to_s,
|
|
||||||
"websiteKey" => site_key,
|
|
||||||
"recaptchaDataSValue" => s_value,
|
|
||||||
},
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
captcha_client.close
|
|
||||||
|
|
||||||
raise response["error"].as_s if response["error"]?
|
|
||||||
task_id = response["taskId"].as_i
|
|
||||||
|
|
||||||
loop do
|
|
||||||
sleep 10.seconds
|
|
||||||
|
|
||||||
response = JSON.parse(captcha_client.post("/getTaskResult",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"taskId" => task_id,
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
if response["status"]?.try &.== "ready"
|
|
||||||
break
|
|
||||||
elsif response["errorId"]?.try &.as_i != 0
|
|
||||||
raise response["errorDescription"].as_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
|
|
||||||
headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
|
|
||||||
response = YT_POOL.client &.post("/sorry/index", headers: headers, form: inputs)
|
|
||||||
headers = HTTP::Headers{
|
|
||||||
"Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0],
|
|
||||||
}
|
|
||||||
cookies = HTTP::Cookies.from_client_headers(headers)
|
|
||||||
|
|
||||||
cookies.each { |cookie| CONFIG.cookies << cookie }
|
|
||||||
|
|
||||||
# Persist cookies between runs
|
|
||||||
File.write("config/config.yml", CONFIG.to_yaml)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue ex
|
|
||||||
LOGGER.error("BypassCaptchaJob: #{ex.message}")
|
|
||||||
ensure
|
|
||||||
sleep 1.minute
|
|
||||||
Fiber.yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -18,6 +18,13 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
|||||||
"updatedAt" => Time.utc.to_unix,
|
"updatedAt" => Time.utc.to_unix,
|
||||||
"lastChannelRefreshedAt" => 0_i64,
|
"lastChannelRefreshedAt" => 0_i64,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# "totalRequests" => 0_i64,
|
||||||
|
# "successfulRequests" => 0_i64
|
||||||
|
# "ratio" => 0_i64
|
||||||
|
#
|
||||||
|
"playback" => {} of String => Int64 | Float64,
|
||||||
}
|
}
|
||||||
|
|
||||||
private getter db : DB::Database
|
private getter db : DB::Database
|
||||||
@ -30,7 +37,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
|||||||
|
|
||||||
loop do
|
loop do
|
||||||
refresh_stats
|
refresh_stats
|
||||||
sleep 1.minute
|
sleep 10.minute
|
||||||
Fiber.yield
|
Fiber.yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -56,5 +63,8 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
|||||||
"updatedAt" => Time.utc.to_unix,
|
"updatedAt" => Time.utc.to_unix,
|
||||||
"lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64,
|
"lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Reset playback requests tracker
|
||||||
|
STATISTICS["playback"] = {} of String => Int64 | Float64
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,6 +6,22 @@ module Invidious::Routes::API::V1::Misc
|
|||||||
if !CONFIG.statistics_enabled
|
if !CONFIG.statistics_enabled
|
||||||
return {"software" => SOFTWARE}.to_json
|
return {"software" => SOFTWARE}.to_json
|
||||||
else
|
else
|
||||||
|
# Calculate playback success rate
|
||||||
|
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]?)
|
||||||
|
tracker = tracker.as(Hash(String, Int64 | Float64))
|
||||||
|
|
||||||
|
if !tracker.empty?
|
||||||
|
total_requests = tracker["totalRequests"]
|
||||||
|
success_count = tracker["successfulRequests"]
|
||||||
|
|
||||||
|
if total_requests.zero?
|
||||||
|
tracker["ratio"] = 1_i64
|
||||||
|
else
|
||||||
|
tracker["ratio"] = (success_count / (total_requests)).round(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
|
return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -122,5 +122,11 @@ module Invidious::Routes::BeforeAll
|
|||||||
end
|
end
|
||||||
|
|
||||||
env.set "current_page", URI.encode_www_form(current_page)
|
env.set "current_page", URI.encode_www_form(current_page)
|
||||||
|
|
||||||
|
unregistered_path_whitelist = {"/", "/login", "/licenses", "/privacy"}
|
||||||
|
if !env.get?("user") && !unregistered_path_whitelist.includes?(env.request.path) && CONFIG.login_only
|
||||||
|
env.response.headers["Location"] = "/login"
|
||||||
|
haltf env, status_code: 302
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -80,9 +80,14 @@ module Invidious::Routes::VideoPlayback
|
|||||||
# Remove the Range header added previously.
|
# Remove the Range header added previously.
|
||||||
headers.delete("Range") if range_header.nil?
|
headers.delete("Range") if range_header.nil?
|
||||||
|
|
||||||
|
playback_statistics = get_playback_statistic()
|
||||||
|
playback_statistics["totalRequests"] += 1
|
||||||
|
|
||||||
if response.status_code >= 400
|
if response.status_code >= 400
|
||||||
env.response.content_type = "text/plain"
|
env.response.content_type = "text/plain"
|
||||||
haltf env, response.status_code
|
haltf env, response.status_code
|
||||||
|
else
|
||||||
|
playback_statistics["successfulRequests"] += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
if url.includes? "&file=seg.ts"
|
if url.includes? "&file=seg.ts"
|
||||||
|
@ -227,8 +227,22 @@ struct Video
|
|||||||
info.dig?("streamingData", "hlsManifestUrl").try &.as_s
|
info.dig?("streamingData", "hlsManifestUrl").try &.as_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def dash_manifest_url
|
def dash_manifest_url : String?
|
||||||
info.dig?("streamingData", "dashManifestUrl").try &.as_s
|
raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s
|
||||||
|
return nil if raw_dash_url.nil?
|
||||||
|
|
||||||
|
# Use manifest v5 parameter to reduce file size
|
||||||
|
# See https://github.com/iv-org/invidious/issues/4186
|
||||||
|
dash_url = URI.parse(raw_dash_url)
|
||||||
|
dash_query = dash_url.query || ""
|
||||||
|
|
||||||
|
if dash_query.empty?
|
||||||
|
dash_url.path = "#{dash_url.path}/mpd_version/5"
|
||||||
|
else
|
||||||
|
dash_url.query = "#{dash_query}&mpd_version=5"
|
||||||
|
end
|
||||||
|
|
||||||
|
return dash_url.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def genre_url : String?
|
def genre_url : String?
|
||||||
|
@ -78,6 +78,11 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
|
|||||||
# YouTube may return a different video player response than expected.
|
# YouTube may return a different video player response than expected.
|
||||||
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
||||||
# Line to be reverted if one day we solve the video not available issue.
|
# Line to be reverted if one day we solve the video not available issue.
|
||||||
|
|
||||||
|
# Although technically not a call to /videoplayback the fact that YouTube is returning the
|
||||||
|
# wrong video means that we should count it as a failure.
|
||||||
|
get_playback_statistic()["totalRequests"] += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||||
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
|
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
def add_yt_headers(request)
|
def add_yt_headers(request)
|
||||||
if request.headers["User-Agent"] == "Crystal"
|
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
|
||||||
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
||||||
end
|
|
||||||
|
|
||||||
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
|
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
|
||||||
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||||
@ -37,7 +36,7 @@ struct YoutubeConnectionPool
|
|||||||
conn.close
|
conn.close
|
||||||
conn = HTTP::Client.new(url)
|
conn = HTTP::Client.new(url)
|
||||||
|
|
||||||
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
|
conn.family = CONFIG.force_resolve
|
||||||
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||||
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
response = yield conn
|
response = yield conn
|
||||||
@ -52,7 +51,7 @@ struct YoutubeConnectionPool
|
|||||||
private def build_pool
|
private def build_pool
|
||||||
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
|
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
|
||||||
conn = HTTP::Client.new(url)
|
conn = HTTP::Client.new(url)
|
||||||
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
|
conn.family = CONFIG.force_resolve
|
||||||
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||||
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
conn
|
conn
|
||||||
@ -62,7 +61,7 @@ end
|
|||||||
|
|
||||||
def make_client(url : URI, region = nil)
|
def make_client(url : URI, region = nil)
|
||||||
client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure)
|
client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure)
|
||||||
client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC
|
client.family = CONFIG.force_resolve
|
||||||
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
client.read_timeout = 10.seconds
|
client.read_timeout = 10.seconds
|
||||||
client.connect_timeout = 10.seconds
|
client.connect_timeout = 10.seconds
|
||||||
|
Loading…
x
Reference in New Issue
Block a user