mirror of
https://git.nadeko.net/Fijxu/invidious.git
synced 2025-12-14 17:15:09 +00:00
Merge remote-tracking branch 'upstream/master'
Some checks failed
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Some checks failed
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
This commit is contained in:
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@@ -1,6 +1,3 @@
|
|||||||
# Default and lowest precedence. If none of the below matches, @iv-org/developers would be requested for review.
|
|
||||||
* @iv-org/developers
|
|
||||||
|
|
||||||
docker-compose.yml @unixfox
|
docker-compose.yml @unixfox
|
||||||
docker/ @unixfox
|
docker/ @unixfox
|
||||||
kubernetes/ @unixfox
|
kubernetes/ @unixfox
|
||||||
|
|||||||
57
.github/workflows/build-nightly-container.yml
vendored
57
.github/workflows/build-nightly-container.yml
vendored
@@ -17,16 +17,26 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux/amd64
|
||||||
|
name: "AMD64"
|
||||||
|
dockerfile: "docker/Dockerfile"
|
||||||
|
tag_suffix: ""
|
||||||
|
# GitHub doesn't have a ubuntu-latest-arm runner
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64/v8
|
||||||
|
name: "ARM64"
|
||||||
|
dockerfile: "docker/Dockerfile.arm64"
|
||||||
|
tag_suffix: "-arm64"
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
platforms: arm64
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -43,45 +53,22 @@ jobs:
|
|||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: quay.io/invidious/invidious
|
images: quay.io/invidious/invidious
|
||||||
|
flavor: |
|
||||||
|
suffix=${{ matrix.tag_suffix }}
|
||||||
tags: |
|
tags: |
|
||||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
labels: |
|
labels: |
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker AMD64 image for Push Event
|
- name: Build and push Docker ${{ matrix.name }} image for Push Event
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: ${{ matrix.dockerfile }}
|
||||||
platforms: linux/amd64
|
platforms: ${{ matrix.platform }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
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=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
|
||||||
labels: |
|
|
||||||
quay.expires-after=12w
|
|
||||||
|
|
||||||
- name: Build and push Docker ARM64 image for Push Event
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: docker/Dockerfile.arm64
|
|
||||||
platforms: linux/arm64/v8
|
|
||||||
labels: ${{ steps.meta-arm64.outputs.labels }}
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta-arm64.outputs.tags }}
|
|
||||||
build-args: |
|
|
||||||
"release=1"
|
|
||||||
|
|||||||
57
.github/workflows/build-stable-container.yml
vendored
57
.github/workflows/build-stable-container.yml
vendored
@@ -8,16 +8,26 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux/amd64
|
||||||
|
name: "AMD64"
|
||||||
|
dockerfile: "docker/Dockerfile"
|
||||||
|
tag_suffix: ""
|
||||||
|
# GitHub doesn't have a ubuntu-latest-arm runner
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
platform: linux/arm64/v8
|
||||||
|
name: "ARM64"
|
||||||
|
dockerfile: "docker/Dockerfile.arm64"
|
||||||
|
tag_suffix: "-arm64"
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
platforms: arm64
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -36,46 +46,21 @@ jobs:
|
|||||||
images: quay.io/invidious/invidious
|
images: quay.io/invidious/invidious
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
|
suffix=${{ matrix.tag_suffix }}
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
labels: |
|
labels: |
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker AMD64 image for Push Event
|
- name: Build and push Docker ${{ matrix.name }} image for Push Event
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: ${{ matrix.dockerfile }}
|
||||||
platforms: linux/amd64
|
platforms: ${{ matrix.platform }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
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: |
|
|
||||||
latest=false
|
|
||||||
suffix=-arm64
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=raw,value=latest
|
|
||||||
labels: |
|
|
||||||
quay.expires-after=12w
|
|
||||||
|
|
||||||
- name: Build and push Docker ARM64 image for Push Event
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: docker/Dockerfile.arm64
|
|
||||||
platforms: linux/arm64/v8
|
|
||||||
labels: ${{ steps.meta-arm64.outputs.labels }}
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta-arm64.outputs.tags }}
|
|
||||||
build-args: |
|
|
||||||
"release=1"
|
|
||||||
|
|||||||
59
.github/workflows/ci.yml
vendored
59
.github/workflows/ci.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
|||||||
stable: false
|
stable: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -83,46 +83,43 @@ jobs:
|
|||||||
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
|
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
name: "AMD64"
|
||||||
|
# GitHub doesn't have a ubuntu-latest-arm runner
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
name: "ARM64"
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
name: Test ${{ matrix.name }} Docker build
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Use ARM64 Dockerfile if ARM64
|
||||||
|
if: ${{ matrix.name }} == "ARM64"
|
||||||
|
run: sed -i 's/Dockerfile/Dockerfile.arm64/' docker-compose.yml
|
||||||
|
|
||||||
- name: Build Docker
|
- name: Build Docker
|
||||||
run: docker compose build --build-arg release=0
|
run: docker compose build
|
||||||
|
|
||||||
|
- name: Change hmac_key on docker-compose.yml
|
||||||
|
run: sed -i '/hmac_key/s/CHANGE_ME!!/docker-build-hmac-key/' docker-compose.yml
|
||||||
|
|
||||||
- name: Run Docker
|
- name: Run Docker
|
||||||
run: docker compose up -d
|
run: docker compose up -d
|
||||||
|
|
||||||
- name: Test Docker
|
- name: Test Docker
|
||||||
run: while curl -Isf http://localhost:3000; do sleep 1; done
|
id: test
|
||||||
|
run: curl -If http://localhost:3000 --retry 5 --retry-delay 1 --retry-all-errors
|
||||||
|
|
||||||
build-docker-arm64:
|
- name: Print Invidious container logs
|
||||||
|
# Tells Github Actions to always run this step regardless of whether the previous step has failed
|
||||||
runs-on: ubuntu-latest
|
# Without this expression this step would simply be skipped when the previous step fails.
|
||||||
|
if: success() || steps.test.conclusion == 'failure'
|
||||||
steps:
|
run: docker compose logs
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
with:
|
|
||||||
platforms: arm64
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build Docker ARM64 image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: docker/Dockerfile.arm64
|
|
||||||
platforms: linux/arm64/v8
|
|
||||||
build-args: release=0
|
|
||||||
|
|
||||||
- name: Test Docker
|
|
||||||
run: while curl -Isf http://localhost:3000; do sleep 1; done
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
|
|
||||||
@@ -131,7 +128,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ ul.vjs-menu-content::-webkit-scrollbar {
|
|||||||
background-color: rgba(0, 0, 0, 0.75) !important;
|
background-color: rgba(0, 0, 0, 0.75) !important;
|
||||||
border-radius: 9px !important;
|
border-radius: 9px !important;
|
||||||
padding: 5px !important;
|
padding: 5px !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vjs-play-control,
|
.vjs-play-control,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function create_notification_stream(subscriptions) {
|
|||||||
function update_ticker_count() {
|
function update_ticker_count() {
|
||||||
var notification_ticker = document.getElementById('notification_ticker');
|
var notification_ticker = document.getElementById('notification_ticker');
|
||||||
|
|
||||||
const notification_count = helpers.storage.get(STORAGE_KEY_STREAM);
|
const notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0;
|
||||||
if (notification_count > 0) {
|
if (notification_count > 0) {
|
||||||
notification_ticker.innerHTML =
|
notification_ticker.innerHTML =
|
||||||
'<span id="notification_count">' + notification_count + '</span> <i class="icon ion-ios-notifications"></i>';
|
'<span id="notification_count">' + notification_count + '</span> <i class="icon ion-ios-notifications"></i>';
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
|||||||
var options = {
|
var options = {
|
||||||
liveui: true,
|
liveui: true,
|
||||||
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
||||||
|
fontPercent: [0.5, 0.75, 1.25, 1.5, 1.75, 2, 3, 4],
|
||||||
|
windowOpacity: ['0', '0.5', '1'],
|
||||||
|
textOpacity: ['0.5', '1'],
|
||||||
|
persistTextTrackSettings: true,
|
||||||
controlBar: {
|
controlBar: {
|
||||||
children: [
|
children: [
|
||||||
'playToggle',
|
'playToggle',
|
||||||
@@ -184,7 +188,7 @@ var shareOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (location.pathname.startsWith('/embed/')) {
|
if (location.pathname.startsWith('/embed/')) {
|
||||||
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
|
var overlay_content = '<h1><a rel="noopener noreferrer" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
|
||||||
player.overlay({
|
player.overlay({
|
||||||
overlays: [
|
overlays: [
|
||||||
{ start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'},
|
{ start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'},
|
||||||
@@ -450,7 +454,7 @@ if (!video_data.params.listen && video_data.params.annotations) {
|
|||||||
if (target === 'current') {
|
if (target === 'current') {
|
||||||
location.href = path;
|
location.href = path;
|
||||||
} else if (target === 'new') {
|
} else if (target === 'new') {
|
||||||
open(path, '_blank');
|
open(path, '_blank', 'noopener,noreferrer');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -585,6 +589,13 @@ const toggle_captions = (function () {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// For real-time updates to captions (if currently showing)
|
||||||
|
function update_captions() {
|
||||||
|
if (document.body.querySelector('.vjs-text-track-cue')) {
|
||||||
|
toggle_captions(); toggle_captions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggle_fullscreen() {
|
function toggle_fullscreen() {
|
||||||
player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen();
|
player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen();
|
||||||
}
|
}
|
||||||
@@ -597,6 +608,34 @@ function increase_playback_rate(steps) {
|
|||||||
player.playbackRate(options.playbackRates[newIndex]);
|
player.playbackRate(options.playbackRates[newIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function increase_caption_size(steps) {
|
||||||
|
const maxIndex = options.fontPercent.length - 1;
|
||||||
|
const fontPercent = player.textTrackSettings.getValues().fontPercent || 1.25;
|
||||||
|
const curIndex = options.fontPercent.indexOf(fontPercent);
|
||||||
|
let newIndex = curIndex + steps;
|
||||||
|
newIndex = helpers.clamp(newIndex, 0, maxIndex);
|
||||||
|
player.textTrackSettings.setValues({ fontPercent: options.fontPercent[newIndex] });
|
||||||
|
update_captions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_caption_window() {
|
||||||
|
const numOptions = options.windowOpacity.length;
|
||||||
|
const windowOpacity = player.textTrackSettings.getValues().windowOpacity || '0';
|
||||||
|
const curIndex = options.windowOpacity.indexOf(windowOpacity);
|
||||||
|
const newIndex = (curIndex + 1) % numOptions;
|
||||||
|
player.textTrackSettings.setValues({ windowOpacity: options.windowOpacity[newIndex] });
|
||||||
|
update_captions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_caption_opacity() {
|
||||||
|
const numOptions = options.textOpacity.length;
|
||||||
|
const textOpacity = player.textTrackSettings.getValues().textOpacity || '1';
|
||||||
|
const curIndex = options.textOpacity.indexOf(textOpacity);
|
||||||
|
const newIndex = (curIndex + 1) % numOptions;
|
||||||
|
player.textTrackSettings.setValues({ textOpacity: options.textOpacity[newIndex] });
|
||||||
|
update_captions();
|
||||||
|
}
|
||||||
|
|
||||||
addEventListener('keydown', function (e) {
|
addEventListener('keydown', function (e) {
|
||||||
if (e.target.tagName.toLowerCase() === 'input') {
|
if (e.target.tagName.toLowerCase() === 'input') {
|
||||||
// Ignore input when focus is on certain elements, e.g. form fields.
|
// Ignore input when focus is on certain elements, e.g. form fields.
|
||||||
@@ -693,6 +732,12 @@ addEventListener('keydown', function (e) {
|
|||||||
case '>': action = increase_playback_rate.bind(this, 1); break;
|
case '>': action = increase_playback_rate.bind(this, 1); break;
|
||||||
case '<': action = increase_playback_rate.bind(this, -1); break;
|
case '<': action = increase_playback_rate.bind(this, -1); break;
|
||||||
|
|
||||||
|
case '=': action = increase_caption_size.bind(this, 1); break;
|
||||||
|
case '-': action = increase_caption_size.bind(this, -1); break;
|
||||||
|
|
||||||
|
case 'w': action = toggle_caption_window; break;
|
||||||
|
case 'o': action = toggle_caption_opacity; break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.info('Unhandled key down event: %s:', decoratedKey, e);
|
console.info('Unhandled key down event: %s:', decoratedKey, e);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ function get_reddit_comments() {
|
|||||||
</b> \
|
</b> \
|
||||||
</p> \
|
</p> \
|
||||||
<b> \
|
<b> \
|
||||||
<a rel="noopener" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \
|
<a rel="noopener noreferrer" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \
|
||||||
</b> \
|
</b> \
|
||||||
</div> \
|
</div> \
|
||||||
<div>{contentHtml}</div> \
|
<div>{contentHtml}</div> \
|
||||||
|
|||||||
@@ -928,7 +928,7 @@ default_user_preferences:
|
|||||||
##
|
##
|
||||||
## Default dash video quality.
|
## Default dash video quality.
|
||||||
##
|
##
|
||||||
## Note: this setting only takes effet if the
|
## Note: this setting only takes effect if the
|
||||||
## 'quality' parameter is set to "dash".
|
## 'quality' parameter is set to "dash".
|
||||||
##
|
##
|
||||||
## Accepted values:
|
## Accepted values:
|
||||||
|
|||||||
@@ -75,14 +75,19 @@ end
|
|||||||
|
|
||||||
HMAC_KEY = CONFIG.hmac_key
|
HMAC_KEY = CONFIG.hmac_key
|
||||||
|
|
||||||
PG_DB = DB.open CONFIG.database_url
|
PG_DB = begin
|
||||||
|
DB.open CONFIG.database_url
|
||||||
ARCHIVE_URL = URI.parse("https://archive.org")
|
rescue ex
|
||||||
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
puts "Failed to connect to PostgreSQL database: #{ex.cause.try &.message}"
|
||||||
REDDIT_URL = URI.parse("https://www.reddit.com")
|
puts "Check your 'config.yml' database settings or PostgreSQL settings."
|
||||||
YT_URL = URI.parse("https://www.youtube.com")
|
exit(1)
|
||||||
|
end
|
||||||
|
ARCHIVE_URL = URI.parse("https://archive.org")
|
||||||
|
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||||
|
REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||||
|
YT_URL = URI.parse("https://www.youtube.com")
|
||||||
PUBSUB_HOST_URL = CONFIG.pubsub_domain
|
PUBSUB_HOST_URL = CONFIG.pubsub_domain
|
||||||
HOST_URL = make_host_url(Kemal.config)
|
HOST_URL = make_host_url(Kemal.config)
|
||||||
|
|
||||||
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
||||||
@@ -249,8 +254,8 @@ error 404 do |env|
|
|||||||
Invidious::Routes::ErrorRoutes.error_404(env)
|
Invidious::Routes::ErrorRoutes.error_404(env)
|
||||||
end
|
end
|
||||||
|
|
||||||
error 500 do |env, ex|
|
error 500 do |env, exception|
|
||||||
error_template(500, ex)
|
error_template(500, exception)
|
||||||
end
|
end
|
||||||
|
|
||||||
static_headers do |env|
|
static_headers do |env|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ private IMAGE_QUALITIES = {320, 560, 640, 1280, 2000}
|
|||||||
# TODO: Add "sort_by"
|
# TODO: Add "sort_by"
|
||||||
def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
|
def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
|
||||||
if cursor.nil?
|
if cursor.nil?
|
||||||
# Egljb21tdW5pdHk%3D is the protobuf object to load "community"
|
# EgVwb3N0c_IGBAoCSgA%3D is the protobuf object to load "posts"
|
||||||
initial_data = YoutubeAPI.browse(ucid, params: "Egljb21tdW5pdHk%3D")
|
initial_data = YoutubeAPI.browse(ucid, params: "EgVwb3N0c_IGBAoCSgA%3D")
|
||||||
|
|
||||||
items = [] of JSON::Any
|
items = [] of JSON::Any
|
||||||
extract_items(initial_data) do |item|
|
extract_items(initial_data) do |item|
|
||||||
@@ -24,15 +24,21 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
|
|||||||
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode)
|
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def decode_ucid_from_post_protobuf(params)
|
||||||
|
decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
|
||||||
|
.try { |i| Base64.decode(i) }
|
||||||
|
.try { |i| IO::Memory.new(i) }
|
||||||
|
.try { |i| Protodec::Any.parse(i) }
|
||||||
|
|
||||||
|
return decoded_protobuf.try(&.["56:0:embedded"]["2:0:string"].as_s)
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
|
def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
|
||||||
object = {
|
object = {
|
||||||
"2:string" => "community",
|
"56:embedded" => {
|
||||||
"25:embedded" => {
|
"2:string" => ucid,
|
||||||
"22:string" => post_id.to_s,
|
"3:string" => post_id.to_s,
|
||||||
},
|
"11:string" => ucid,
|
||||||
"45:embedded" => {
|
|
||||||
"2:varint" => 1_i64,
|
|
||||||
"3:varint" => 1_i64,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
params = object.try { |i| Protodec::Any.cast_json(i) }
|
params = object.try { |i| Protodec::Any.cast_json(i) }
|
||||||
@@ -40,7 +46,7 @@ def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
|
|||||||
.try { |i| Base64.urlsafe_encode(i) }
|
.try { |i| Base64.urlsafe_encode(i) }
|
||||||
.try { |i| URI.encode_www_form(i) }
|
.try { |i| URI.encode_www_form(i) }
|
||||||
|
|
||||||
initial_data = YoutubeAPI.browse(ucid, params: params)
|
initial_data = YoutubeAPI.browse("FEpost_detail", params: params)
|
||||||
|
|
||||||
items = [] of JSON::Any
|
items = [] of JSON::Any
|
||||||
extract_items(initial_data) do |item|
|
extract_items(initial_data) do |item|
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by)
|
|||||||
case sort_by
|
case sort_by
|
||||||
when "last", "last_added"
|
when "last", "last_added"
|
||||||
# Equivalent to "&sort=lad"
|
# Equivalent to "&sort=lad"
|
||||||
# {"2:string": "playlists", "3:varint": 4, "4:varint": 1, "6:varint": 1}
|
# {"2:string": "playlists", "3:varint": 4, "4:varint": 1, "6:varint": 1, "110:embedded": {"1:embedded": {"8:string": ""}}}
|
||||||
"EglwbGF5bGlzdHMYBCABMAE%3D"
|
"EglwbGF5bGlzdHMYBCABMAHyBgQKAkIA"
|
||||||
when "oldest", "oldest_created"
|
when "oldest", "oldest_created"
|
||||||
# formerly "&sort=da"
|
# formerly "&sort=da"
|
||||||
# Not available anymore :c or maybe ??
|
# Not available anymore :c or maybe ??
|
||||||
# {"2:string": "playlists", "3:varint": 2, "4:varint": 1, "6:varint": 1}
|
# {"2:string": "playlists", "3:varint": 2, "4:varint": 1, "6:varint": 1, "110:embedded": {"1:embedded": {"8:string": ""}}}
|
||||||
"EglwbGF5bGlzdHMYAiABMAE%3D"
|
"EglwbGF5bGlzdHMYAiABMAHyBgQKAkIA"
|
||||||
# {"2:string": "playlists", "3:varint": 1, "4:varint": 1, "6:varint": 1}
|
# {"2:string": "playlists", "3:varint": 1, "4:varint": 1, "6:varint": 1}
|
||||||
# "EglwbGF5bGlzdHMYASABMAE%3D"
|
# "EglwbGF5bGlzdHMYASABMAE%3D"
|
||||||
when "newest", "newest_created"
|
when "newest", "newest_created"
|
||||||
# Formerly "&sort=dd"
|
# Formerly "&sort=dd"
|
||||||
# {"2:string": "playlists", "3:varint": 3, "4:varint": 1, "6:varint": 1}
|
# {"2:string": "playlists", "3:varint": 3, "4:varint": 1, "6:varint": 1, "110:embedded": {"1:embedded": {"8:string": ""}}}
|
||||||
"EglwbGF5bGlzdHMYAyABMAE%3D"
|
"EglwbGF5bGlzdHMYAyABMAHyBgQKAkIA"
|
||||||
end
|
end
|
||||||
|
|
||||||
initial_data = YoutubeAPI.browse(ucid, params: params || "")
|
initial_data = YoutubeAPI.browse(ucid, params: params || "")
|
||||||
|
|||||||
@@ -16,23 +16,27 @@ module Invidious::Comments
|
|||||||
return parse_youtube(id, response, format, locale, thin_mode, sort_by)
|
return parse_youtube(id, response, format, locale, thin_mode, sort_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_community_post_comments(ucid, post_id)
|
def fetch_community_post_comments(ucid, post_id, sort_by = "top")
|
||||||
|
case sort_by
|
||||||
|
when "top"
|
||||||
|
sort_by_val = 0_i64
|
||||||
|
when "new", "newest"
|
||||||
|
sort_by_val = 1_i64
|
||||||
|
else # top
|
||||||
|
sort_by_val = 0_i64
|
||||||
|
end
|
||||||
|
|
||||||
object = {
|
object = {
|
||||||
"2:string" => "community",
|
"2:string" => "posts",
|
||||||
"25:embedded" => {
|
|
||||||
"22:string" => post_id,
|
|
||||||
},
|
|
||||||
"45:embedded" => {
|
|
||||||
"2:varint" => 1_i64,
|
|
||||||
"3:varint" => 1_i64,
|
|
||||||
},
|
|
||||||
"53:embedded" => {
|
"53:embedded" => {
|
||||||
"4:embedded" => {
|
"4:embedded" => {
|
||||||
"6:varint" => 0_i64,
|
"6:varint" => sort_by_val,
|
||||||
"27:varint" => 1_i64,
|
"15:varint" => 2_i64,
|
||||||
|
"25:varint" => 0_i64,
|
||||||
"29:string" => post_id,
|
"29:string" => post_id,
|
||||||
"30:string" => ucid,
|
"30:string" => ucid,
|
||||||
},
|
},
|
||||||
|
"7:varint" => 0_i64,
|
||||||
"8:string" => "comments-section",
|
"8:string" => "comments-section",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -43,7 +47,7 @@ module Invidious::Comments
|
|||||||
|
|
||||||
object2 = {
|
object2 = {
|
||||||
"80226972:embedded" => {
|
"80226972:embedded" => {
|
||||||
"2:string" => ucid,
|
"2:string" => "FEcomment_post_detail_page_web_top_level",
|
||||||
"3:string" => object_parsed,
|
"3:string" => object_parsed,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -320,6 +324,15 @@ module Invidious::Comments
|
|||||||
end
|
end
|
||||||
|
|
||||||
def produce_continuation(video_id, cursor = "", sort_by = "top")
|
def produce_continuation(video_id, cursor = "", sort_by = "top")
|
||||||
|
case sort_by
|
||||||
|
when "top"
|
||||||
|
sort_by_val = 0_i64
|
||||||
|
when "new", "newest"
|
||||||
|
sort_by_val = 1_i64
|
||||||
|
else # top
|
||||||
|
sort_by_val = 0_i64
|
||||||
|
end
|
||||||
|
|
||||||
object = {
|
object = {
|
||||||
"2:embedded" => {
|
"2:embedded" => {
|
||||||
"2:string" => video_id,
|
"2:string" => video_id,
|
||||||
@@ -340,21 +353,12 @@ module Invidious::Comments
|
|||||||
"1:string" => cursor,
|
"1:string" => cursor,
|
||||||
"4:embedded" => {
|
"4:embedded" => {
|
||||||
"4:string" => video_id,
|
"4:string" => video_id,
|
||||||
"6:varint" => 0_i64,
|
"6:varint" => sort_by_val,
|
||||||
},
|
},
|
||||||
"5:varint" => 20_i64,
|
"5:varint" => 20_i64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
case sort_by
|
|
||||||
when "top"
|
|
||||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
|
|
||||||
when "new", "newest"
|
|
||||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 1_i64
|
|
||||||
else # top
|
|
||||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
|
|
||||||
end
|
|
||||||
|
|
||||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||||
.try { |i| Protodec::Any.from_json(i) }
|
.try { |i| Protodec::Any.from_json(i) }
|
||||||
.try { |i| Base64.urlsafe_encode(i) }
|
.try { |i| Base64.urlsafe_encode(i) }
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ module Invidious::Frontend::WatchPage
|
|||||||
str << " class=\"pure-form pure-form-stacked\""
|
str << " class=\"pure-form pure-form-stacked\""
|
||||||
str << " action='#{url}'"
|
str << " action='#{url}'"
|
||||||
str << " method='post'"
|
str << " method='post'"
|
||||||
str << " rel='noopener'"
|
str << " rel='noopener noreferrer'"
|
||||||
str << " target='_blank'>"
|
str << " target='_blank'>"
|
||||||
str << '\n'
|
str << '\n'
|
||||||
|
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ module Invidious::Routes::API::V1::Channels
|
|||||||
if ucid.nil?
|
if ucid.nil?
|
||||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
||||||
return error_json(400, "Invalid post ID") if response["error"]?
|
return error_json(400, "Invalid post ID") if response["error"]?
|
||||||
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
|
ucid = decode_ucid_from_post_protobuf(response.dig("endpoint", "browseEndpoint", "params").as_s)
|
||||||
else
|
else
|
||||||
ucid = ucid.to_s
|
ucid = ucid.to_s
|
||||||
end
|
end
|
||||||
@@ -460,13 +460,15 @@ module Invidious::Routes::API::V1::Channels
|
|||||||
|
|
||||||
format = env.params.query["format"]?
|
format = env.params.query["format"]?
|
||||||
format ||= "json"
|
format ||= "json"
|
||||||
|
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||||
|
sort_by ||= "top"
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
case continuation
|
case continuation
|
||||||
when nil, ""
|
when nil, ""
|
||||||
ucid = env.params.query["ucid"]
|
ucid = env.params.query["ucid"]
|
||||||
comments = Comments.fetch_community_post_comments(ucid, id)
|
comments = Comments.fetch_community_post_comments(ucid, id, sort_by: sort_by)
|
||||||
else
|
else
|
||||||
comments = YoutubeAPI.browse(continuation: continuation)
|
comments = YoutubeAPI.browse(continuation: continuation)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -190,15 +190,30 @@ module Invidious::Routes::API::V1::Misc
|
|||||||
|
|
||||||
sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint
|
sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint
|
||||||
params = sub_endpoint.try &.dig?("params")
|
params = sub_endpoint.try &.dig?("params")
|
||||||
|
|
||||||
|
if sub_endpoint["browseId"]?.try &.as_s == "FEpost_detail"
|
||||||
|
decoded_protobuf = params.try &.as_s.try { |i| URI.decode_www_form(i) }
|
||||||
|
.try { |i| Base64.decode(i) }
|
||||||
|
.try { |i| IO::Memory.new(i) }
|
||||||
|
.try { |i| Protodec::Any.parse(i) }
|
||||||
|
|
||||||
|
ucid = decoded_protobuf.try(&.["56:0:embedded"]["2:0:string"].as_s)
|
||||||
|
post_id = decoded_protobuf.try(&.["56:0:embedded"]["3:1:string"].as_s)
|
||||||
|
else
|
||||||
|
ucid = sub_endpoint["browseId"]? if sub_endpoint["browseId"]? && sub_endpoint["browseId"]?.try &.as_s.starts_with? "UC"
|
||||||
|
post_id = nil
|
||||||
|
end
|
||||||
rescue ex
|
rescue ex
|
||||||
return error_json(500, ex)
|
return error_json(500, ex)
|
||||||
end
|
end
|
||||||
JSON.build do |json|
|
JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
|
json.field "browseId", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
|
||||||
|
json.field "ucid", ucid if ucid != nil
|
||||||
json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
|
json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
|
||||||
json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]?
|
json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]?
|
||||||
json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]?
|
json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]?
|
||||||
|
json.field "postId", post_id if post_id != nil
|
||||||
json.field "params", params.try &.as_s
|
json.field "params", params.try &.as_s
|
||||||
json.field "pageType", page_type
|
json.field "pageType", page_type
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ module Invidious::Routes::Channels
|
|||||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
||||||
return error_template(400, "Invalid post ID") if response["error"]?
|
return error_template(400, "Invalid post ID") if response["error"]?
|
||||||
|
|
||||||
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
|
ucid = decode_ucid_from_post_protobuf(response.dig("endpoint", "browseEndpoint", "params").as_s)
|
||||||
post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
|
post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ module Invidious::Routes::Embed
|
|||||||
return error_template(500, ex)
|
return error_template(500, ex)
|
||||||
end
|
end
|
||||||
|
|
||||||
url = "/embed/#{first_playlist_video}?#{env.params.query}"
|
url = "/embed/#{first_playlist_video.id}?#{env.params.query}"
|
||||||
|
|
||||||
if env.params.query.size > 0
|
if env.params.query.size > 0
|
||||||
url += "?#{env.params.query}"
|
url += "?#{env.params.query}"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ def extract_video_info(video_id : String, env : HTTP::Server::Context | Nil = ni
|
|||||||
"reason" => JSON::Any.new(reason),
|
"reason" => JSON::Any.new(reason),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
elsif video_id != player_response.dig("videoDetails", "videoId")
|
elsif video_id != player_response.dig?("videoDetails", "videoId")
|
||||||
# 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.
|
||||||
@@ -109,21 +109,34 @@ def extract_video_info(video_id : String, env : HTTP::Server::Context | Nil = ni
|
|||||||
params["reason"] = JSON::Any.new(reason) if reason
|
params["reason"] = JSON::Any.new(reason) if reason
|
||||||
|
|
||||||
if !CONFIG.invidious_companion.present?
|
if !CONFIG.invidious_companion.present?
|
||||||
if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
|
if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
|
||||||
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
|
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
|
||||||
players_fallback = [YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile]
|
players_fallback = {YoutubeAPI::ClientType::TvSimply, YoutubeAPI::ClientType::WebMobile}
|
||||||
|
|
||||||
players_fallback.each do |player_fallback|
|
players_fallback.each do |player_fallback|
|
||||||
client_config.client_type = player_fallback
|
client_config.client_type = player_fallback
|
||||||
player_fallback_response = try_fetch_streaming_data(video_id, client_config, env)
|
|
||||||
if player_fallback_response && player_fallback_response["streamingData"]? &&
|
next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config, env))
|
||||||
player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
|
|
||||||
|
adaptive_formats = player_fallback_response.dig?("streamingData", "adaptiveFormats")
|
||||||
|
if adaptive_formats && (adaptive_formats.dig?(0, "url") || adaptive_formats.dig?(0, "signatureCipher"))
|
||||||
streaming_data = player_response["streamingData"].as_h
|
streaming_data = player_response["streamingData"].as_h
|
||||||
streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
|
streaming_data["adaptiveFormats"] = adaptive_formats
|
||||||
player_response["streamingData"] = JSON::Any.new(streaming_data)
|
player_response["streamingData"] = JSON::Any.new(streaming_data)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
rescue InfoException
|
||||||
|
next LOGGER.warn("Failed to fetch streams with #{player_fallback}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Seems like video page can still render even without playable streams.
|
||||||
|
# its better than nothing.
|
||||||
|
#
|
||||||
|
# # Were we able to find playable video streams?
|
||||||
|
# if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
|
||||||
|
# # No :(
|
||||||
|
# end
|
||||||
end
|
end
|
||||||
|
|
||||||
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
||||||
@@ -134,7 +147,11 @@ def extract_video_info(video_id : String, env : HTTP::Server::Context | Nil = ni
|
|||||||
if streaming_data = player_response["streamingData"]?
|
if streaming_data = player_response["streamingData"]?
|
||||||
%w[formats adaptiveFormats].each do |key|
|
%w[formats adaptiveFormats].each do |key|
|
||||||
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
||||||
format.as_h["url"] = JSON::Any.new(convert_url(format))
|
format = format.as_h
|
||||||
|
if format["url"]?.nil?
|
||||||
|
format["url"] = format["signatureCipher"]
|
||||||
|
end
|
||||||
|
format["url"] = JSON::Any.new(convert_url(format))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -154,7 +171,7 @@ def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConf
|
|||||||
playability_status = response["playabilityStatus"]["status"]
|
playability_status = response["playabilityStatus"]["status"]
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
||||||
|
|
||||||
if id != response.dig("videoDetails", "videoId")
|
if id != response.dig?("videoDetails", "videoId")
|
||||||
# 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
|
||||||
raise InfoException.new(
|
raise InfoException.new(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="import_youtube">
|
<label for="import_youtube">
|
||||||
<a rel="noopener" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md">
|
<a rel="noopener noreferrer" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md">
|
||||||
<%= translate(locale, "Import YouTube subscriptions") %>
|
<%= translate(locale, "Import YouTube subscriptions") %>
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module YoutubeAPI
|
|||||||
extend self
|
extend self
|
||||||
|
|
||||||
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
||||||
private ANDROID_APP_VERSION = "19.32.34"
|
private ANDROID_APP_VERSION = "19.35.36"
|
||||||
private ANDROID_VERSION = "12"
|
private ANDROID_VERSION = "13"
|
||||||
private ANDROID_USER_AGENT = "com.google.android.youtube/#{ANDROID_APP_VERSION} (Linux; U; Android #{ANDROID_VERSION}; US) gzip"
|
private ANDROID_USER_AGENT = "com.google.android.youtube/#{ANDROID_APP_VERSION} (Linux; U; Android #{ANDROID_VERSION}; en_US; SM-S908E Build/TP1A.220624.014) gzip"
|
||||||
private ANDROID_SDK_VERSION = 31_i64
|
private ANDROID_SDK_VERSION = 33_i64
|
||||||
|
|
||||||
private ANDROID_TS_APP_VERSION = "1.9"
|
private ANDROID_TS_APP_VERSION = "1.9"
|
||||||
private ANDROID_TS_USER_AGENT = "com.google.android.youtube/1.9 (Linux; U; Android 12; US) gzip"
|
private ANDROID_TS_USER_AGENT = "com.google.android.youtube/1.9 (Linux; U; Android 12; US) gzip"
|
||||||
@@ -17,9 +17,9 @@ module YoutubeAPI
|
|||||||
# For Apple device names, see https://gist.github.com/adamawolf/3048717
|
# For Apple device names, see https://gist.github.com/adamawolf/3048717
|
||||||
# For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
|
# For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
|
||||||
# then go to the dedicated article of the major version you want.
|
# then go to the dedicated article of the major version you want.
|
||||||
private IOS_APP_VERSION = "19.32.8"
|
private IOS_APP_VERSION = "20.11.6"
|
||||||
private IOS_USER_AGENT = "com.google.ios.youtube/#{IOS_APP_VERSION} (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)"
|
private IOS_USER_AGENT = "com.google.ios.youtube/#{IOS_APP_VERSION} (iPhone14,5; U; CPU iOS 18_5 like Mac OS X;)"
|
||||||
private IOS_VERSION = "17.6.1.21G93" # Major.Minor.Patch.Build
|
private IOS_VERSION = "18.5.0.22F76" # Major.Minor.Patch.Build
|
||||||
|
|
||||||
private WINDOWS_VERSION = "10.0"
|
private WINDOWS_VERSION = "10.0"
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ module YoutubeAPI
|
|||||||
WebEmbeddedPlayer
|
WebEmbeddedPlayer
|
||||||
WebMobile
|
WebMobile
|
||||||
WebScreenEmbed
|
WebScreenEmbed
|
||||||
|
WebCreator
|
||||||
|
|
||||||
Android
|
Android
|
||||||
AndroidEmbeddedPlayer
|
AndroidEmbeddedPlayer
|
||||||
@@ -41,6 +42,7 @@ module YoutubeAPI
|
|||||||
|
|
||||||
TvHtml5
|
TvHtml5
|
||||||
TvHtml5ScreenEmbed
|
TvHtml5ScreenEmbed
|
||||||
|
TvSimply
|
||||||
end
|
end
|
||||||
|
|
||||||
# List of hard-coded values used by the different clients
|
# List of hard-coded values used by the different clients
|
||||||
@@ -48,7 +50,7 @@ module YoutubeAPI
|
|||||||
ClientType::Web => {
|
ClientType::Web => {
|
||||||
name: "WEB",
|
name: "WEB",
|
||||||
name_proto: "1",
|
name_proto: "1",
|
||||||
version: "2.20240814.00.00",
|
version: "2.20250222.10.00",
|
||||||
screen: "WATCH_FULL_SCREEN",
|
screen: "WATCH_FULL_SCREEN",
|
||||||
os_name: "Windows",
|
os_name: "Windows",
|
||||||
os_version: WINDOWS_VERSION,
|
os_version: WINDOWS_VERSION,
|
||||||
@@ -57,7 +59,7 @@ module YoutubeAPI
|
|||||||
ClientType::WebEmbeddedPlayer => {
|
ClientType::WebEmbeddedPlayer => {
|
||||||
name: "WEB_EMBEDDED_PLAYER",
|
name: "WEB_EMBEDDED_PLAYER",
|
||||||
name_proto: "56",
|
name_proto: "56",
|
||||||
version: "1.20240812.01.00",
|
version: "1.20250219.01.00",
|
||||||
screen: "EMBED",
|
screen: "EMBED",
|
||||||
os_name: "Windows",
|
os_name: "Windows",
|
||||||
os_version: WINDOWS_VERSION,
|
os_version: WINDOWS_VERSION,
|
||||||
@@ -66,7 +68,7 @@ module YoutubeAPI
|
|||||||
ClientType::WebMobile => {
|
ClientType::WebMobile => {
|
||||||
name: "MWEB",
|
name: "MWEB",
|
||||||
name_proto: "2",
|
name_proto: "2",
|
||||||
version: "2.20240813.02.00",
|
version: "2.20250224.01.00",
|
||||||
os_name: "Android",
|
os_name: "Android",
|
||||||
os_version: ANDROID_VERSION,
|
os_version: ANDROID_VERSION,
|
||||||
platform: "MOBILE",
|
platform: "MOBILE",
|
||||||
@@ -74,12 +76,20 @@ module YoutubeAPI
|
|||||||
ClientType::WebScreenEmbed => {
|
ClientType::WebScreenEmbed => {
|
||||||
name: "WEB",
|
name: "WEB",
|
||||||
name_proto: "1",
|
name_proto: "1",
|
||||||
version: "2.20240814.00.00",
|
version: "2.20250222.10.00",
|
||||||
screen: "EMBED",
|
screen: "EMBED",
|
||||||
os_name: "Windows",
|
os_name: "Windows",
|
||||||
os_version: WINDOWS_VERSION,
|
os_version: WINDOWS_VERSION,
|
||||||
platform: "DESKTOP",
|
platform: "DESKTOP",
|
||||||
},
|
},
|
||||||
|
ClientType::WebCreator => {
|
||||||
|
name: "WEB_CREATOR",
|
||||||
|
name_proto: "62",
|
||||||
|
version: "1.20241203.01.00",
|
||||||
|
os_name: "Windows",
|
||||||
|
os_version: WINDOWS_VERSION,
|
||||||
|
platform: "DESKTOP",
|
||||||
|
},
|
||||||
|
|
||||||
# Android
|
# Android
|
||||||
|
|
||||||
@@ -161,7 +171,7 @@ module YoutubeAPI
|
|||||||
ClientType::TvHtml5 => {
|
ClientType::TvHtml5 => {
|
||||||
name: "TVHTML5",
|
name: "TVHTML5",
|
||||||
name_proto: "7",
|
name_proto: "7",
|
||||||
version: "7.20240813.07.00",
|
version: "7.20250219.14.00",
|
||||||
},
|
},
|
||||||
ClientType::TvHtml5ScreenEmbed => {
|
ClientType::TvHtml5ScreenEmbed => {
|
||||||
name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
|
name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
|
||||||
@@ -169,6 +179,11 @@ module YoutubeAPI
|
|||||||
version: "2.0",
|
version: "2.0",
|
||||||
screen: "EMBED",
|
screen: "EMBED",
|
||||||
},
|
},
|
||||||
|
ClientType::TvSimply => {
|
||||||
|
name: "TVHTML5_SIMPLY",
|
||||||
|
name_proto: "74",
|
||||||
|
version: "1.0",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
####################################################################
|
####################################################################
|
||||||
|
|||||||
Reference in New Issue
Block a user