mirror of
https://github.com/iv-org/invidious.git
synced 2026-07-25 15:53:20 +05:30
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb93ddc66b | ||
|
|
5510657e80 | ||
|
|
adfec7646b | ||
|
|
30c0656a93 | ||
|
|
e5bde4bc60 | ||
|
|
c84acc10ad | ||
|
|
029a496578 | ||
|
|
1111ea7294 | ||
|
|
9e8d102c94 | ||
|
|
6373ac7814 | ||
|
|
31ecdd85f9 | ||
|
|
9c62c8b660 | ||
|
|
d61dd7205c | ||
|
|
77ad41678b | ||
|
|
ab099e46cf | ||
|
|
fe2b6dbd67 | ||
|
|
efb9269e58 |
@@ -1,44 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve Invidious
|
||||
title: '[Bug] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
BEFORE TRYING TO REPORT A BUG:
|
||||
|
||||
* Read the FAQ: https://docs.invidious.io/faq/!
|
||||
* Use the search function to check if there is already an issue open for your problem: https://github.com/search?q=repo%3Aiv-org%2Finvidious+replace+me+with+your+bug&type=issues!
|
||||
|
||||
MAKE SURE TO FOLLOW THE TWO STEPS ABOVE BEFORE REPORTING A BUG. A BUG THAT ALREADY EXIST WILL IMMEDIATELY CLOSED.
|
||||
|
||||
If you want to suggest a new feature please use "Feature request" instead
|
||||
If you want to suggest an enhancement to an existing feature please use "Enhancement" instead
|
||||
-->
|
||||
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Steps to Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
**Logs**
|
||||
<!-- If applicable, copy the log that appear in the browser page where the error is reported. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here.
|
||||
- Browser (if applicable):
|
||||
- OS (if applicable):
|
||||
-->
|
||||
@@ -0,0 +1,103 @@
|
||||
name: Bug report
|
||||
description: Create a bug report to help us improve Invidious
|
||||
title: "[Bug] "
|
||||
labels: ["bug"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
BEFORE TRYING TO REPORT A BUG:
|
||||
|
||||
* Read the FAQ: https://docs.invidious.io/faq/
|
||||
* Use the search function to check if there is already an issue open for your problem: https://github.com/search?q=repo%3Aiv-org%2Finvidious+replace+me+with+your+bug&type=issues
|
||||
|
||||
MAKE SURE TO FOLLOW THE TWO STEPS ABOVE BEFORE REPORTING A BUG. A BUG THAT ALREADY EXISTS WILL BE IMMEDIATELY CLOSED.
|
||||
|
||||
If you want to suggest a new feature, please use "Feature request" instead
|
||||
If you want to suggest an enhancement to an existing feature, please use "Enhancement" instead
|
||||
- type: textarea
|
||||
id: describe-bug
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: If applicable, copy the logs that appear on the page where the error is reported.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
placeholder: |
|
||||
- Browser (if applicable):
|
||||
- OS (if applicable):
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: ai-policy
|
||||
attributes:
|
||||
label: AI Policy
|
||||
options:
|
||||
- label: I have read the [AI Policy](https://github.com/iv-org/invidious/blob/master/AI_POLICY.md) and understand the disclosure requirements
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: ai-usage
|
||||
attributes:
|
||||
label: AI usage
|
||||
description: Did you use AI to help create this issue?
|
||||
options:
|
||||
- AI was not used to create this issue
|
||||
- AI was used to fully create this issue
|
||||
- AI was used to partially create this issue (explain how)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: ai-models
|
||||
attributes:
|
||||
label: Model(s) used
|
||||
description: If you used AI, list the EXACT model(s) used (and thinking/reasoning level if relevant). Leave empty if you did not use AI.
|
||||
placeholder: "e.g. GLM-5.2, MiniMax-M3, DeepSeek V4 Pro, Kimi K2.7 Code..."
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: ai-tools
|
||||
attributes:
|
||||
label: Tool(s) used
|
||||
description: If you used AI, list the tool(s) used to interact with it. Leave empty if you did not use AI.
|
||||
placeholder: "e.g. OpenCode, OpenChamber, Cline, Pi, Open WebUI..."
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: ai-explanation
|
||||
attributes:
|
||||
label: How was AI used?
|
||||
description: If you selected "AI was used to partially create this issue (explain how)" above, explain how AI was used here. Leave empty otherwise.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Enhancement
|
||||
about: Suggest an enhancement for an existing feature
|
||||
title: '[Enhancement] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use the search function to check if the desired function has already been requested by someone else -->
|
||||
<!-- If you want to suggest a new feature please use "Feature request" instead -->
|
||||
<!-- If you want to report a bug, please use "Bug report" instead -->
|
||||
|
||||
**Is your enhancement request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the enhancement here. -->
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Enhancement
|
||||
description: Suggest an enhancement for an existing feature
|
||||
title: "[Enhancement] "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please use the search function to check if the desired function has already been requested by someone else
|
||||
If you want to suggest a new feature, please use "Feature request" instead
|
||||
If you want to report a bug, please use "Bug report" instead
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your enhancement request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the enhancement here.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: ai-policy
|
||||
attributes:
|
||||
label: AI Policy
|
||||
options:
|
||||
- label: I have read the [AI Policy](https://github.com/iv-org/invidious/blob/master/AI_POLICY.md) and understand the disclosure requirements
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: ai-usage
|
||||
attributes:
|
||||
label: AI usage
|
||||
description: Did you use AI to help create this issue?
|
||||
options:
|
||||
- AI was not used to create this issue
|
||||
- AI was used to fully create this issue
|
||||
- AI was used to partially create this issue (explain how)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: ai-models
|
||||
attributes:
|
||||
label: Model(s) used
|
||||
description: If you used AI, list the EXACT model(s) used (and thinking/reasoning level if relevant). Leave empty if you did not use AI.
|
||||
placeholder: "e.g. GLM-5.2, MiniMax-M3, DeepSeek V4 Pro, Kimi K2.7 Code..."
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: ai-tools
|
||||
attributes:
|
||||
label: Tool(s) used
|
||||
description: If you used AI, list the tool(s) used to interact with it. Leave empty if you did not use AI.
|
||||
placeholder: "e.g. OpenCode, OpenChamber, Cline, Pi, Open WebUI..."
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: ai-explanation
|
||||
attributes:
|
||||
label: How was AI used?
|
||||
description: If you selected "AI was used to partially create this issue (explain how)" above, explain how AI was used here. Leave empty otherwise.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: '[Feature request] '
|
||||
labels: feature-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use the search function to check if the desired function has already been requested by someone else -->
|
||||
<!-- If you want to suggest an enhancement to an existing feature please use "Enhancement" instead -->
|
||||
<!-- If you want to report a bug, please use "Bug report" instead -->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
@@ -0,0 +1,81 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature request] "
|
||||
labels: ["feature-request"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please use the search function to check if the desired function has already been requested by someone else
|
||||
If you want to suggest an enhancement to an existing feature, please use "Enhancement" instead
|
||||
If you want to report a bug, please use "Bug report" instead
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: ai-policy
|
||||
attributes:
|
||||
label: AI Policy
|
||||
options:
|
||||
- label: I have read the [AI Policy](https://github.com/iv-org/invidious/blob/master/AI_POLICY.md) and understand the disclosure requirements
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: ai-usage
|
||||
attributes:
|
||||
label: AI usage
|
||||
description: Did you use AI to help create this issue?
|
||||
options:
|
||||
- AI was not used to create this issue
|
||||
- AI was used to fully create this issue
|
||||
- AI was used to partially create this issue (explain how)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: ai-models
|
||||
attributes:
|
||||
label: Model(s) used
|
||||
description: If you used AI, list the EXACT model(s) used (and thinking/reasoning level if relevant). Leave empty if you did not use AI.
|
||||
placeholder: "e.g. GLM-5.2, MiniMax-M3, DeepSeek V4 Pro, Kimi K2.7 Code..."
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: ai-tools
|
||||
attributes:
|
||||
label: Tool(s) used
|
||||
description: If you used AI, list the tool(s) used to interact with it. Leave empty if you did not use AI.
|
||||
placeholder: "e.g. OpenCode, OpenChamber, Cline, Pi, Open WebUI..."
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: ai-explanation
|
||||
attributes:
|
||||
label: How was AI used?
|
||||
description: If you selected "AI was used to partially create this issue (explain how)" above, explain how AI was used here. Leave empty otherwise.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,27 @@
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read the [AI Policy](https://github.com/iv-org/invidious/blob/master/AI_POLICY.md) and understand the disclosure requirements
|
||||
|
||||
|
||||
## AI Disclosure
|
||||
|
||||
<!-- Tick exactly one -->
|
||||
|
||||
- [ ] AI was not used to create this pull request
|
||||
- [ ] AI was used to fully create this pull request
|
||||
- [ ] AI was used to partially create this pull request
|
||||
|
||||
<!-- Leave the following section blank if you didn't use AI -->
|
||||
|
||||
**Model(s) used (and thinking/reasoning level if relevant):**
|
||||
<!-- e.g. GLM-5.2, MiniMax-M3, DeepSeek V4 Pro, Kimi K2.7 Code... -->
|
||||
|
||||
**Tool(s) used:**
|
||||
<!-- e.g. OpenCode, OpenChamber, Cline, Pi, Open WebUI... -->
|
||||
|
||||
**How was AI used?**
|
||||
<!-- If you selected "AI was used to partially create this pull request" above, explain how AI was used here. Leave empty otherwise. -->
|
||||
|
||||
---
|
||||
|
||||
## Pull request description
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
# https://github.com/marketplace/actions/docker-manifest-create-action
|
||||
- name: Create and push manifest
|
||||
uses: int128/docker-manifest-create-action@v2.22.0
|
||||
uses: int128/docker-manifest-create-action@v2.25.0
|
||||
with:
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:master
|
||||
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
# https://github.com/marketplace/actions/docker-manifest-create-action
|
||||
- name: Create and push manifest
|
||||
uses: int128/docker-manifest-create-action@v2.22.0
|
||||
uses: int128/docker-manifest-create-action@v2.25.0
|
||||
with:
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:latest
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
- name: Cache Shards
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
./lib
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
crystal: latest
|
||||
|
||||
- name: Cache Shards
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
./lib
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Create new release
|
||||
|
||||
# Manually-triggered workflow that prepares a new Invidious release by running
|
||||
# scripts/create-release.py. All the logic lives in that script (which can also
|
||||
# be run by hand); this workflow only provides checkout, Python and secrets.
|
||||
#
|
||||
# The script computes the version, collects every merged PR since the last
|
||||
# release, generates the changelog prose via an OpenAI-compatible API (any
|
||||
# endpoint via OPENAI_BASE_URL; OpenRouter by default), bumps shard.yml +
|
||||
# CHANGELOG.md, and opens a release PR. Tagging and publishing stay manual, as
|
||||
# the release documentation requires maintainer review.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
major:
|
||||
description: "Major version. Empty = keep current. Bump only for breaking changes."
|
||||
required: false
|
||||
default: ""
|
||||
patch:
|
||||
description: "Patch number (increment for a 2nd release on the same UTC day)."
|
||||
required: false
|
||||
default: "0"
|
||||
model:
|
||||
description: "Model used to write the changelog."
|
||||
required: false
|
||||
default: "deepseek/deepseek-v4-pro"
|
||||
base_url:
|
||||
description: "OpenAI-compatible API base URL (default: OpenRouter)."
|
||||
required: false
|
||||
default: "https://openrouter.ai/api/v1"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Prepare release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_BASE_URL: ${{ inputs.base_url }}
|
||||
run: |
|
||||
python3 scripts/create-release.py \
|
||||
${{ inputs.major && format('--major {0}', inputs.major) || '' }} \
|
||||
--patch "${{ inputs.patch }}" \
|
||||
--model "${{ inputs.model }}"
|
||||
@@ -0,0 +1,38 @@
|
||||
Invidious is and has always been made by human first and foremost. However, things have changed recently with the rise of AI.
|
||||
|
||||
This document is going to explain everything that you need to know if you ever contribute in any way to Invidious using any kind of AI.
|
||||
|
||||
This document has been fully written, from scratch, by a Human.
|
||||
|
||||
|
||||
# Motivation
|
||||
|
||||
Invidious is written in an obscure language: Crystal.
|
||||
|
||||
Because it is obscure the number of people knowing it is really low.
|
||||
|
||||
Because the amount of people knowing it is really low, the amount of contributors to Invidious is really low: the vast majority of contributions are done by the Invidious maintainers themselves.
|
||||
|
||||
Invidious is the biggest Crystal project that exists, bigger than Crystal itself [(yes, seriously)](https://shards.info/).
|
||||
|
||||
The problem of being the biggest software in an obscure language is that you're often effectively the first project to encounter a problem and because it's an obscure language, not a lot of libraries exist to make it easier for you, meaning, you usually have to make everything you need yourself.
|
||||
|
||||
This makes it so working on Invidious far harder than working on most open source projects because you are effectively not benefiting and not using any external libraries for the vast majority of things. Almost any time you need anything, you have to make it yourself, which overcomplicates everything.
|
||||
|
||||
We are aware that some people wont like this change and we might even end up on one of the "bad people" list, but we try to be reasonable. We ask that you, please, do not fork the project out of spite because of this new policy - let's not split the thin list of people able to contribute even thinner. Contributions are welcome and highly preferred to anything made by AI.
|
||||
|
||||
This policy comes from a place of *need* not from a place of *choice*.
|
||||
|
||||
|
||||
# Policy
|
||||
|
||||
|
||||
Now that AIs exists and have become *reasonably good*, we will tolerate people using them with reasons and knowledge, as long those rules are respected:
|
||||
|
||||
|
||||
- **Any one using AI to report bugs or submit code MUST properly disclose it, this includes mentioning the name of the EXACT model used and the tools used to interact with it.**
|
||||
- The Human using AI MUST properly check the output manually in addition to any automated check that may exist or may have been created, **this includes BOTH codes AND bug reports**.
|
||||
- Any code submitted by a Human, written even partially by AI, is the responsibility of this Human - If it's malicious, broken, destructive or anything bad, the Human is the sole responsible.
|
||||
- Any new code touching any of the actual functions of Invidious MUST BE thoroughly tested by the Human MANUALLY.
|
||||
- Team members using AIs are strongly encouraged to wait for the review of another Human before merging anything.
|
||||
- At any point [Human-in-the-loop](https://en.wikipedia.org/wiki/Human-in-the-loop) applies.
|
||||
@@ -2,6 +2,58 @@
|
||||
|
||||
## vX.Y.0 (future)
|
||||
|
||||
|
||||
## v2.20260723.0
|
||||
|
||||
### Wrap-up
|
||||
|
||||
This release hardens security, adds an instance option to disable API endpoints, and fixes YouTube backend changes that broke video metadata and playback. A cross-user playlist deletion vulnerability was closed, and videos are no longer incorrectly marked as upcoming.
|
||||
|
||||
Instance owners gain a new configuration flag to disable API endpoints that are easy to abuse, a reduced backoff in the channel refresh job when no errors occur, and an updated User-Agent header for YouTube requests. The OCI image bumps OpenSSL to 3.6.3, and the Docker image moves to Crystal 1.20.3.
|
||||
|
||||
Developer experience improves with the ability to release directly from GitHub Actions, enhanced issue/PR templates with an AI policy compliance field, and a new `AI_POLICY.md` document. Dependency bumps keep CI current with `int128/docker-manifest-create-action` 2.25.0 and `actions/cache` 6.
|
||||
|
||||
### New features & important changes
|
||||
#### For Users
|
||||
- No user-facing changes in this release.
|
||||
|
||||
#### For instance owners
|
||||
- A new configuration option allows disabling API endpoints that are easy to abuse (#5630)
|
||||
- The `RefreshChannelsJob` backoff is reduced when no errors occur, improving channel refresh efficiency (#5759)
|
||||
- The User-Agent header for YouTube requests was updated to maintain compatibility (#5794)
|
||||
- OpenSSL was updated to 3.6.3 in the OCI image (#5808)
|
||||
- The Docker image now uses Crystal 1.20.3 (#5797)
|
||||
|
||||
#### For developers
|
||||
- Releases can now be triggered directly from a GitHub Action workflow (#5823)
|
||||
- Issue and PR templates were reworded and enhanced, and a field for AI policy compliance was added (#5804, #5803)
|
||||
- An `AI_POLICY.md` document was created to clarify the project's stance on AI-generated contributions (#5791)
|
||||
- CI dependencies were bumped: `int128/docker-manifest-create-action` to 2.25.0 and `actions/cache` to 6 (#5807, #5793)
|
||||
|
||||
### Bugs fixed
|
||||
#### User-side
|
||||
- A security issue that allowed playlist deletion across different users was fixed (#5790)
|
||||
- Videos are no longer incorrectly labelled as `isUpcoming = true` due to a YouTube backend change (#5800)
|
||||
- YouTube backend API changes that caused playback and metadata issues were addressed (#5818, #5819)
|
||||
|
||||
### Full list of pull requests merged since the last release (newest first)
|
||||
|
||||
* feat: add ability to release from github action (https://github.com/iv-org/invidious/pull/5823, by @unixfox)
|
||||
* youtube backend api fix (https://github.com/iv-org/invidious/pull/5818, by @gabe4278)
|
||||
* Hotfix - Fix YouTube change (https://github.com/iv-org/invidious/pull/5819, by @bunchy7ss)
|
||||
* chore: update openssl to 3.6.3 in OCI (https://github.com/iv-org/invidious/pull/5808, by @Fijxu)
|
||||
* chore(deps): bump int128/docker-manifest-create-action from 2.22.0 to 2.25.0 (https://github.com/iv-org/invidious/pull/5807, by @dependabot[bot])
|
||||
* Various enhancements and rewording to the issues/PRs templates (https://github.com/iv-org/invidious/pull/5804, by @TheFrenchGhosty)
|
||||
* Add a field for AI policy compliance in the issues/PRs templates (https://github.com/iv-org/invidious/pull/5803, by @TheFrenchGhosty)
|
||||
* Fix videos being incorrectly labelled as isUpcoming = true (https://github.com/iv-org/invidious/pull/5800, by @absidue)
|
||||
* chore(deps): bump 84codes/crystal from 1.20.2-alpine to 1.20.3-alpine in /docker (https://github.com/iv-org/invidious/pull/5797, by @dependabot[bot])
|
||||
* Create AI_POLICY.md (https://github.com/iv-org/invidious/pull/5791, by @TheFrenchGhosty)
|
||||
* Add option to disable easy to abuse API endpoints (https://github.com/iv-org/invidious/pull/5630, by @Fijxu)
|
||||
* [RefreshChannelsJob] Reduce backoff if no errors occur (https://github.com/iv-org/invidious/pull/5759, by @unrealtournament)
|
||||
* fix: security issue playlist deletion cross user (https://github.com/iv-org/invidious/pull/5790, by @unixfox)
|
||||
* chore: update User-Agent header for Youtube requests (https://github.com/iv-org/invidious/pull/5794, by @Fijxu)
|
||||
* chore(deps): bump actions/cache from 5 to 6 (https://github.com/iv-org/invidious/pull/5793, by @dependabot[bot])
|
||||
|
||||
## v2.20260626.0
|
||||
|
||||
### Wrap-up
|
||||
|
||||
+15
-13
@@ -205,7 +205,6 @@ https_only: false
|
||||
# path: /tmp/invidious.sock
|
||||
# permissions: 777
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Network (outbound)
|
||||
# -----------------------------
|
||||
@@ -228,7 +227,6 @@ https_only: false
|
||||
##
|
||||
#pool_size: 100
|
||||
|
||||
|
||||
##
|
||||
## Additional cookies to be sent when requesting the youtube API.
|
||||
##
|
||||
@@ -263,7 +261,6 @@ https_only: false
|
||||
# host:
|
||||
# port:
|
||||
|
||||
|
||||
##
|
||||
## Use Innertube's transcripts API instead of timedtext for closed captions
|
||||
##
|
||||
@@ -344,7 +341,6 @@ https_only: false
|
||||
##
|
||||
#statistics_enabled: false
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Users and accounts
|
||||
# -----------------------------
|
||||
@@ -456,12 +452,25 @@ full_refresh: false
|
||||
##
|
||||
feed_threads: 1
|
||||
|
||||
##
|
||||
## Setting to disable easy to abuse API endpoints that can
|
||||
## be spammed and therefore blocking your Invidious instance.
|
||||
##
|
||||
## Useful for public instance maintainers.
|
||||
##
|
||||
## Notes: The following API endpoints will be disabled:
|
||||
## - /api/v1/videos
|
||||
## - /api/v1/clips
|
||||
## - /api/v1/transcripts
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: false
|
||||
##
|
||||
disable_abusable_api: false
|
||||
|
||||
jobs:
|
||||
|
||||
## Options for the database cleaning job
|
||||
clear_expired_items:
|
||||
|
||||
## Enable/Disable job
|
||||
##
|
||||
## Accepted values: true, false
|
||||
@@ -471,7 +480,6 @@ jobs:
|
||||
|
||||
## Options for the channels updater job
|
||||
refresh_channels:
|
||||
|
||||
## Enable/Disable job
|
||||
##
|
||||
## Accepted values: true, false
|
||||
@@ -481,7 +489,6 @@ jobs:
|
||||
|
||||
## Options for the RSS feeds updater job
|
||||
refresh_feeds:
|
||||
|
||||
## Enable/Disable job
|
||||
##
|
||||
## Accepted values: true, false
|
||||
@@ -489,7 +496,6 @@ jobs:
|
||||
##
|
||||
enable: true
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Miscellaneous
|
||||
# -----------------------------
|
||||
@@ -688,7 +694,6 @@ default_user_preferences:
|
||||
##
|
||||
#captions: ["", "", ""]
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Interface
|
||||
# -----------------------------
|
||||
@@ -790,7 +795,6 @@ default_user_preferences:
|
||||
##
|
||||
#related_videos: true
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Video player behavior
|
||||
# -----------------------------
|
||||
@@ -854,7 +858,6 @@ default_user_preferences:
|
||||
##
|
||||
#video_loop: false
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Video playback settings
|
||||
# -----------------------------
|
||||
@@ -966,7 +969,6 @@ default_user_preferences:
|
||||
##
|
||||
#sort: published
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Miscellaneous
|
||||
# -----------------------------
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
# https://github.com/openssl/openssl/releases/tag/openssl-3.6.2
|
||||
ARG OPENSSL_VERSION='3.6.2'
|
||||
ARG OPENSSL_SHA256='aaf51a1fe064384f811daeaeb4ec4dce7340ec8bd893027eee676af31e83a04f'
|
||||
# https://github.com/openssl/openssl/releases/tag/openssl-3.6.3
|
||||
ARG OPENSSL_VERSION='3.6.3'
|
||||
ARG OPENSSL_SHA256='243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1'
|
||||
|
||||
FROM 84codes/crystal:1.20.2-alpine AS dependabot-crystal
|
||||
FROM 84codes/crystal:1.20.3-alpine AS dependabot-crystal
|
||||
|
||||
# We compile openssl ourselves due to a memory leak in how crystal interacts
|
||||
# with openssl
|
||||
|
||||
Executable
+397
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare a new Invidious release.
|
||||
|
||||
This automates the release procedure documented at
|
||||
https://github.com/iv-org/documentation/blob/master/docs/create-new-invidious-release.md
|
||||
|
||||
What it does (all of it, so the GitHub workflow only has to run this script):
|
||||
1. Computes the next version `<MAJOR>.<YYYYMMDD>.<PATCH>` from shard.yml.
|
||||
2. Collects EVERY pull request merged since the previous release, via `gh`.
|
||||
3. Asks an LLM (OpenRouter API) to write only the changelog *prose*
|
||||
(wrap-up + categorised sections), using recent CHANGELOG.md as a style
|
||||
guide. The exhaustive PR list is generated deterministically so no PR is
|
||||
ever missed.
|
||||
4. Bumps shard.yml and prepends the new section to CHANGELOG.md.
|
||||
5. Creates the `release-vX.Y.Z` branch, commits, pushes, and opens a PR to
|
||||
master with the post-merge checklist (tagging + publishing stay manual,
|
||||
as the docs require maintainer review and forbid squash-merges/retagging).
|
||||
|
||||
The LLM is called through any OpenAI-compatible Chat Completions endpoint,
|
||||
configured with the standard `OPENAI_API_KEY` / `OPENAI_BASE_URL` environment
|
||||
variables (as used by the official OpenAI SDK). It defaults to OpenRouter, but
|
||||
point it at OpenAI, a local server, etc. by setting `OPENAI_BASE_URL`.
|
||||
|
||||
Manual usage:
|
||||
export OPENAI_API_KEY=sk-or-... # OpenRouter key by default
|
||||
./scripts/create-release.py # full run: edits, commit, push, PR
|
||||
./scripts/create-release.py --dry-run # only edit files locally
|
||||
./scripts/create-release.py --major 3 # bump major (breaking changes)
|
||||
./scripts/create-release.py --patch 1 # 2nd release on the same day
|
||||
./scripts/create-release.py --model x/y # override the model
|
||||
# Use vanilla OpenAI instead of OpenRouter:
|
||||
OPENAI_BASE_URL=https://api.openai.com/v1 \
|
||||
./scripts/create-release.py --model gpt-4o
|
||||
|
||||
Requirements: python3, git, and the GitHub CLI (`gh`) authenticated (or
|
||||
GH_TOKEN set). Only OPENAI_API_KEY is mandatory (skippable with --dry-run
|
||||
combined with --pr-json for offline testing).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
|
||||
DEFAULT_MODEL = "deepseek/deepseek-v4-pro"
|
||||
REPO_URL = "https://github.com/iv-org/invidious"
|
||||
CHANGELOG_FILE = "CHANGELOG.md"
|
||||
SHARD_FILE = "shard.yml"
|
||||
|
||||
|
||||
def die(msg):
|
||||
prefix = "::error::" if os.environ.get("GITHUB_ACTIONS") else "error: "
|
||||
print(f"{prefix}{msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run(cmd, capture=False, check=True):
|
||||
"""Run a subprocess command (list of args)."""
|
||||
print(f"$ {' '.join(cmd)}", file=sys.stderr)
|
||||
result = subprocess.run(
|
||||
cmd, check=False, text=True,
|
||||
stdout=subprocess.PIPE if capture else None,
|
||||
stderr=subprocess.PIPE if capture else None,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
detail = (result.stderr or "").strip() if capture else ""
|
||||
die(f"command failed ({result.returncode}): {' '.join(cmd)}\n{detail}")
|
||||
return (result.stdout or "").strip() if capture else ""
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Version + git helpers
|
||||
# --------------------------------------------------------------------------- #
|
||||
def read_current_version():
|
||||
with open(SHARD_FILE, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
m = re.match(r"^version:\s*(\S+)", line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
die(f"no 'version:' line found in {SHARD_FILE}")
|
||||
|
||||
|
||||
def compute_version(args):
|
||||
current = read_current_version() # e.g. 2.20260626.0-dev
|
||||
print(f"Current shard.yml version: {current}", file=sys.stderr)
|
||||
major = str(args.major) if args.major is not None else current.split(".", 1)[0]
|
||||
date = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
|
||||
version = f"{major}.{date}.{args.patch}"
|
||||
tag = f"v{version}"
|
||||
existing = run(["git", "tag", "--list", tag], capture=True)
|
||||
if existing:
|
||||
die(f"tag {tag} already exists; pass --patch {args.patch + 1}")
|
||||
return version, tag
|
||||
|
||||
|
||||
def previous_tag():
|
||||
tags = run(["git", "tag", "--list", "v*", "--sort=-creatordate"],
|
||||
capture=True)
|
||||
return tags.splitlines()[0].strip() if tags else ""
|
||||
|
||||
|
||||
def tag_date(tag):
|
||||
return run(["git", "log", "-1", "--format=%cI", tag], capture=True)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Pull request collection
|
||||
# --------------------------------------------------------------------------- #
|
||||
def collect_prs(repo, prev_tag, pr_json_path):
|
||||
if pr_json_path:
|
||||
with open(pr_json_path, encoding="utf-8") as fh:
|
||||
prs = json.load(fh)
|
||||
else:
|
||||
cmd = ["gh", "pr", "list", "--repo", repo, "--base", "master",
|
||||
"--state", "merged", "--limit", "1000",
|
||||
"--json", "number,title,url,author,mergedAt"]
|
||||
if prev_tag:
|
||||
since = tag_date(prev_tag)
|
||||
print(f"Listing PRs merged after {since} ({prev_tag})",
|
||||
file=sys.stderr)
|
||||
cmd += ["--jq", f'[.[] | select(.mergedAt > "{since}")]']
|
||||
else:
|
||||
print("No previous tag; listing all merged PRs.", file=sys.stderr)
|
||||
out = run(cmd, capture=True)
|
||||
prs = json.loads(out) if out else []
|
||||
prs.sort(key=lambda p: p.get("mergedAt") or "", reverse=True)
|
||||
print(f"Collected {len(prs)} merged PR(s).", file=sys.stderr)
|
||||
return prs
|
||||
|
||||
|
||||
def format_author(author):
|
||||
"""Match the CHANGELOG convention (`gh` returns bots as `app/dependabot`,
|
||||
but the changelog uses `dependabot[bot]`)."""
|
||||
login = (author or {}).get("login") or "ghost"
|
||||
if login.startswith("app/"):
|
||||
return f"{login[len('app/'):]}[bot]"
|
||||
return login
|
||||
|
||||
|
||||
def format_pr_list(prs):
|
||||
lines = []
|
||||
for pr in prs:
|
||||
author = format_author(pr.get("author"))
|
||||
title = (pr.get("title") or "").strip()
|
||||
lines.append(f"* {title} ({REPO_URL}/pull/{pr.get('number')}, "
|
||||
f"by @{author})")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Changelog generation
|
||||
# --------------------------------------------------------------------------- #
|
||||
def extract_style_reference(changelog, max_chars=6000):
|
||||
matches = list(re.finditer(r"^## v\S+.*$", changelog, flags=re.MULTILINE))
|
||||
for i, m in enumerate(matches):
|
||||
if "(future)" in m.group(0):
|
||||
continue
|
||||
start = m.start()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(changelog)
|
||||
section = changelog[start:end].strip()
|
||||
section = re.split(r"^### Full list of pull requests", section,
|
||||
flags=re.MULTILINE)[0].strip()
|
||||
return section[:max_chars]
|
||||
return ""
|
||||
|
||||
|
||||
def build_prompt(prs, style_ref, version):
|
||||
pr_bullets = "\n".join(
|
||||
f"- #{p.get('number')}: {(p.get('title') or '').strip()}" for p in prs)
|
||||
return f"""You are writing the changelog prose for the Invidious release v{version}.
|
||||
|
||||
Below is an example of a PAST release entry from CHANGELOG.md. Match its \
|
||||
Markdown structure, tone and heading style EXACTLY (a "### Wrap-up" narrative \
|
||||
of 2-3 short paragraphs, then "### New features & important changes" with \
|
||||
"#### For Users", "#### For instance owners" and "#### For developers" \
|
||||
sub-sections, then a "### Bugs fixed" section when relevant).
|
||||
|
||||
--- STYLE EXAMPLE START ---
|
||||
{style_ref}
|
||||
--- STYLE EXAMPLE END ---
|
||||
|
||||
Here is the complete list of pull requests merged since the last release, \
|
||||
which you must summarise (reference PRs inline as "(#1234)"). Sort content by \
|
||||
user impact, with new features and breaking changes first:
|
||||
|
||||
{pr_bullets}
|
||||
|
||||
Rules:
|
||||
- Output ONLY the Markdown body starting at "### Wrap-up". Do NOT include the \
|
||||
"## v{version}" header.
|
||||
- Do NOT include the exhaustive "### Full list of pull requests merged since \
|
||||
the last release" section; it is appended separately.
|
||||
- Do NOT invent PRs or changes that are not in the list above.
|
||||
- Keep it concise and factual."""
|
||||
|
||||
|
||||
def call_llm(base_url, api_key, model, prompt):
|
||||
url = base_url.rstrip("/") + "/chat/completions"
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a meticulous release "
|
||||
"manager for the Invidious project who writes clear, accurate "
|
||||
"changelogs in Markdown."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
"temperature": 0.3,
|
||||
}
|
||||
req = urllib.request.Request(
|
||||
url, data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
# OpenRouter-specific attribution headers; ignored by other APIs.
|
||||
"HTTP-Referer": REPO_URL,
|
||||
"X-Title": "Invidious release changelog",
|
||||
}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as resp:
|
||||
body = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
die(f"LLM API HTTP {exc.code}: {exc.read().decode('utf-8', 'replace')}")
|
||||
except urllib.error.URLError as exc:
|
||||
die(f"LLM API request failed: {exc}")
|
||||
try:
|
||||
content = body["choices"][0]["message"]["content"].strip()
|
||||
except (KeyError, IndexError):
|
||||
die(f"unexpected LLM API response: {json.dumps(body)[:1000]}")
|
||||
if not content:
|
||||
die("LLM API returned empty content")
|
||||
return content
|
||||
|
||||
|
||||
def generate_prose(prs, version, base_url, api_key, model):
|
||||
if not prs:
|
||||
return ("### Wrap-up\n\nNo pull requests were merged since the "
|
||||
"previous release.")
|
||||
with open(CHANGELOG_FILE, encoding="utf-8") as fh:
|
||||
changelog = fh.read()
|
||||
prompt = build_prompt(prs, extract_style_reference(changelog), version)
|
||||
return call_llm(base_url, api_key, model, prompt)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# File edits
|
||||
# --------------------------------------------------------------------------- #
|
||||
def update_changelog(version, entry_body):
|
||||
with open(CHANGELOG_FILE, encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
section = f"## v{version}\n\n{entry_body}\n"
|
||||
future = re.search(r"^## v\S*\s*\(future\)\s*$", content, flags=re.MULTILINE)
|
||||
if future:
|
||||
at = future.end()
|
||||
rest = content[at:].lstrip("\n")
|
||||
content = content[:at] + "\n\n" + section + "\n" + rest
|
||||
else:
|
||||
title = re.search(r"^# CHANGELOG\s*$", content, flags=re.MULTILINE)
|
||||
at = title.end() if title else 0
|
||||
content = (content[:at] + "\n\n" + section + "\n"
|
||||
+ content[at:].lstrip("\n"))
|
||||
with open(CHANGELOG_FILE, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
|
||||
|
||||
def update_shard(version):
|
||||
with open(SHARD_FILE, encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
content, n = re.subn(r"^version:\s*.*$", f"version: {version}", content,
|
||||
count=1, flags=re.MULTILINE)
|
||||
if n != 1:
|
||||
die(f"could not update 'version:' in {SHARD_FILE}")
|
||||
with open(SHARD_FILE, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Git / GitHub
|
||||
# --------------------------------------------------------------------------- #
|
||||
def detect_repo(explicit):
|
||||
if explicit:
|
||||
return explicit
|
||||
env = os.environ.get("GITHUB_REPOSITORY")
|
||||
if env:
|
||||
return env
|
||||
url = run(["git", "remote", "get-url", "origin"], capture=True, check=False)
|
||||
m = re.search(r"github\.com[:/](.+?)(?:\.git)?$", url)
|
||||
return m.group(1) if m else "iv-org/invidious"
|
||||
|
||||
|
||||
def create_pull_request(repo, tag, branch, model, summary):
|
||||
if run(["git", "config", "user.name"], capture=True, check=False) == "":
|
||||
run(["git", "config", "user.name", "github-actions[bot]"])
|
||||
run(["git", "config", "user.email",
|
||||
"41898282+github-actions[bot]@users.noreply.github.com"])
|
||||
run(["git", "checkout", "-b", branch])
|
||||
run(["git", "add", CHANGELOG_FILE, SHARD_FILE])
|
||||
run(["git", "commit", "-m", f"Release {tag}"])
|
||||
run(["git", "push", "-u", "origin", branch])
|
||||
|
||||
body = f"""Automated release preparation for **{tag}**.
|
||||
|
||||
Bumps `{SHARD_FILE}` and updates `{CHANGELOG_FILE}` (prose generated by the \
|
||||
`{model}` model; the PR list is generated deterministically so every merged \
|
||||
PR is included).
|
||||
|
||||
## Before merging
|
||||
- [ ] Review the generated changelog for accuracy
|
||||
- [ ] Confirm the version number and major bump (if any)
|
||||
|
||||
## After merging (maintainer, per the release docs)
|
||||
- Do **not** squash-merge; keep the release commit distinct.
|
||||
- Tag the merge commit: `git tag -as {tag}` using the changelog summary as \
|
||||
the annotation, then `git push origin {tag}`.
|
||||
- Run the "Prepare for next release" step (append `-dev` in {SHARD_FILE}, add \
|
||||
a new `(future)` changelog header).
|
||||
- Create the GitHub release from the tag with the changelog summary + PR list.
|
||||
|
||||
Refer to the documentation for more details:
|
||||
https://github.com/iv-org/documentation/blob/master/docs/create-new-invidious-release.md
|
||||
|
||||
### Proposed tag annotation (changelog summary)
|
||||
```markdown
|
||||
{summary}
|
||||
```
|
||||
"""
|
||||
run(["gh", "pr", "create", "--repo", repo, "--base", "master",
|
||||
"--head", branch, "--title", f"Release {tag}", "--body", body])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Prepare an Invidious release.")
|
||||
parser.add_argument("--major", type=int, default=None,
|
||||
help="major version (default: keep current)")
|
||||
parser.add_argument("--patch", type=int, default=0,
|
||||
help="patch number (default: 0)")
|
||||
parser.add_argument("--model", default=os.environ.get(
|
||||
"LLM_MODEL", DEFAULT_MODEL),
|
||||
help=f"model name (default: {DEFAULT_MODEL})")
|
||||
parser.add_argument("--base-url", default=os.environ.get(
|
||||
"OPENAI_BASE_URL", DEFAULT_BASE_URL),
|
||||
help=f"OpenAI-compatible API base URL (default: {DEFAULT_BASE_URL})")
|
||||
parser.add_argument("--repo", default=None,
|
||||
help="owner/repo (default: autodetect)")
|
||||
parser.add_argument("--pr-json", default=None,
|
||||
help="read PR list from a JSON file instead of gh")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="edit files only; no branch/commit/push/PR")
|
||||
args = parser.parse_args()
|
||||
|
||||
api_key = os.environ.get("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
die("OPENAI_API_KEY is not set")
|
||||
|
||||
repo = detect_repo(args.repo)
|
||||
version, tag = compute_version(args)
|
||||
branch = f"release-{tag}"
|
||||
prev = previous_tag()
|
||||
print(f"Preparing {tag} (previous: {prev or '<none>'}) on {repo}",
|
||||
file=sys.stderr)
|
||||
|
||||
prs = collect_prs(repo, prev, args.pr_json)
|
||||
prose = generate_prose(prs, version, args.base_url, api_key, args.model)
|
||||
|
||||
entry_body = prose
|
||||
pr_list = format_pr_list(prs)
|
||||
if pr_list:
|
||||
entry_body += ("\n\n### Full list of pull requests merged since the "
|
||||
"last release (newest first)\n\n" + pr_list)
|
||||
|
||||
update_changelog(version, entry_body)
|
||||
update_shard(version)
|
||||
print(f"Updated {CHANGELOG_FILE} and {SHARD_FILE} for {tag}.",
|
||||
file=sys.stderr)
|
||||
|
||||
if args.dry_run:
|
||||
print(f"\n--- Dry run complete. Review the diff, then:\n"
|
||||
f" git checkout -b {branch}\n"
|
||||
f" git add {CHANGELOG_FILE} {SHARD_FILE}\n"
|
||||
f" git commit -m 'Release {tag}'\n"
|
||||
f" git push -u origin {branch}\n"
|
||||
f" gh pr create --base master --head {branch} "
|
||||
f"--title 'Release {tag}'", file=sys.stderr)
|
||||
return
|
||||
|
||||
create_pull_request(repo, tag, branch, args.model, prose)
|
||||
print(f"\nOpened release PR for {tag}.", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,5 @@
|
||||
name: invidious
|
||||
version: 2.20260626.0
|
||||
version: 2.20260723.0
|
||||
|
||||
authors:
|
||||
- Invidious team <contact@invidious.io>
|
||||
|
||||
@@ -217,6 +217,7 @@ end
|
||||
Kemal.config.powered_by_header = false
|
||||
add_handler FilteredCompressHandler.new
|
||||
add_handler APIHandler.new
|
||||
add_handler DisableAbusableAPIHandler.new
|
||||
add_handler AuthHandler.new
|
||||
add_handler DenyFrame.new
|
||||
|
||||
|
||||
@@ -183,6 +183,9 @@ class Config
|
||||
# Playlist length limit
|
||||
property playlist_length_limit : Int32 = 500
|
||||
|
||||
# Disable easy to abuse API endpoints
|
||||
property disable_abusable_api : Bool = false
|
||||
|
||||
def disabled?(option)
|
||||
case disabled = CONFIG.disable_proxy
|
||||
when Bool
|
||||
|
||||
@@ -194,13 +194,13 @@ module Invidious::Database::PlaylistVideos
|
||||
PG_DB.exec(request, args: video_array)
|
||||
end
|
||||
|
||||
def delete(index)
|
||||
def delete(index, plid : String)
|
||||
request = <<-SQL
|
||||
DELETE FROM playlist_videos *
|
||||
WHERE index = $1
|
||||
WHERE index = $1 AND plid = $2
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, index)
|
||||
PG_DB.exec(request, index, plid)
|
||||
end
|
||||
|
||||
def delete_by_playlist(plid : String)
|
||||
|
||||
@@ -133,6 +133,26 @@ class APIHandler < Kemal::Handler
|
||||
end
|
||||
end
|
||||
|
||||
class DisableAbusableAPIHandler < Kemal::Handler
|
||||
{% for method in %w(GET HEAD) %}
|
||||
# This endpoints make a video request to Invidious companion.
|
||||
{% for endpoint in %w(videos clips transcripts) %}
|
||||
only ["/api/v1/{{ endpoint.id }}/:id"], {{ method }}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
def call(env)
|
||||
return call_next env unless only_match?(env) && CONFIG.disable_abusable_api
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
env.response.status_code = 403
|
||||
message = {"error" => "This API endpoint has been disabled by the administrator."}.to_json
|
||||
env.response.print message
|
||||
env.response.close
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
class DenyFrame < Kemal::Handler
|
||||
exclude ["/embed/*"]
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ struct SearchVideo
|
||||
end
|
||||
|
||||
def upcoming?
|
||||
premiere_timestamp ? true : false
|
||||
premiere_timestamp.try { |t| t.to_unix > 0 } ? true : false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob
|
||||
|
||||
LOGGER.trace("RefreshChannelsJob: #{id} fiber : Updating DB")
|
||||
Invidious::Database::Channels.update_author(id, channel.author)
|
||||
|
||||
if backoff > 2.minutes
|
||||
backoff /= 2
|
||||
LOGGER.debug("RefreshChannelsJob: #{id} fiber : decreasing backoff to #{backoff}s")
|
||||
end
|
||||
rescue ex
|
||||
LOGGER.error("RefreshChannelsJob: #{id} : #{ex.message}")
|
||||
if ex.message == "Deleted or invalid channel"
|
||||
|
||||
@@ -364,7 +364,7 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
return error_json(404, "Playlist does not contain index")
|
||||
end
|
||||
|
||||
Invidious::Database::PlaylistVideos.delete(index)
|
||||
Invidious::Database::PlaylistVideos.delete(index, plid)
|
||||
Invidious::Database::Playlists.update_video_removed(plid, index)
|
||||
|
||||
env.response.status_code = 204
|
||||
|
||||
@@ -357,8 +357,12 @@ module Invidious::Routes::Playlists
|
||||
Invidious::Database::PlaylistVideos.insert(playlist_video)
|
||||
Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
|
||||
when "remove_video"
|
||||
index = env.params.query["set_video_id"]
|
||||
Invidious::Database::PlaylistVideos.delete(index)
|
||||
index = env.params.query["set_video_id"].to_i64?
|
||||
if index.nil? || !playlist.index.includes? index
|
||||
return error_json(404, "Playlist does not contain index")
|
||||
end
|
||||
|
||||
Invidious::Database::PlaylistVideos.delete(index, playlist_id)
|
||||
Invidious::Database::Playlists.update_video_removed(playlist_id, index)
|
||||
when "move_video_before"
|
||||
# TODO: Playlist stub
|
||||
|
||||
@@ -106,7 +106,7 @@ end
|
||||
|
||||
def add_yt_headers(request)
|
||||
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/128.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/149.0.0.0 Safari/537.36"
|
||||
|
||||
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"
|
||||
|
||||
@@ -6,13 +6,13 @@ module YoutubeAPI
|
||||
extend self
|
||||
|
||||
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
||||
private ANDROID_APP_VERSION = "19.35.36"
|
||||
private ANDROID_VERSION = "13"
|
||||
private ANDROID_APP_VERSION = "21.29.366"
|
||||
private ANDROID_VERSION = "16"
|
||||
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 = 33_i64
|
||||
|
||||
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 16; US) gzip"
|
||||
|
||||
# For Apple device names, see https://gist.github.com/adamawolf/3048717
|
||||
# For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
|
||||
@@ -50,7 +50,8 @@ module YoutubeAPI
|
||||
ClientType::Web => {
|
||||
name: "WEB",
|
||||
name_proto: "1",
|
||||
version: "2.20250222.10.00",
|
||||
|
||||
version: "2.20260722.01.00",
|
||||
screen: "WATCH_FULL_SCREEN",
|
||||
os_name: "Windows",
|
||||
os_version: WINDOWS_VERSION,
|
||||
@@ -59,7 +60,7 @@ module YoutubeAPI
|
||||
ClientType::WebEmbeddedPlayer => {
|
||||
name: "WEB_EMBEDDED_PLAYER",
|
||||
name_proto: "56",
|
||||
version: "1.20250219.01.00",
|
||||
version: "2.20260722.01.00",
|
||||
screen: "EMBED",
|
||||
os_name: "Windows",
|
||||
os_version: WINDOWS_VERSION,
|
||||
@@ -68,7 +69,7 @@ module YoutubeAPI
|
||||
ClientType::WebMobile => {
|
||||
name: "MWEB",
|
||||
name_proto: "2",
|
||||
version: "2.20250224.01.00",
|
||||
version: "2.20260722.01.00",
|
||||
os_name: "Android",
|
||||
os_version: ANDROID_VERSION,
|
||||
platform: "MOBILE",
|
||||
@@ -76,7 +77,9 @@ module YoutubeAPI
|
||||
ClientType::WebScreenEmbed => {
|
||||
name: "WEB",
|
||||
name_proto: "1",
|
||||
version: "2.20250222.10.00",
|
||||
|
||||
version: "2.20260722.01.00",
|
||||
|
||||
screen: "EMBED",
|
||||
os_name: "Windows",
|
||||
os_version: WINDOWS_VERSION,
|
||||
@@ -85,7 +88,7 @@ module YoutubeAPI
|
||||
ClientType::WebCreator => {
|
||||
name: "WEB_CREATOR",
|
||||
name_proto: "62",
|
||||
version: "1.20241203.01.00",
|
||||
version: "2.20260722.01.00",
|
||||
os_name: "Windows",
|
||||
os_version: WINDOWS_VERSION,
|
||||
platform: "DESKTOP",
|
||||
@@ -171,7 +174,7 @@ module YoutubeAPI
|
||||
ClientType::TvHtml5 => {
|
||||
name: "TVHTML5",
|
||||
name_proto: "7",
|
||||
version: "7.20250219.14.00",
|
||||
version: "2.20260722.01.00",
|
||||
},
|
||||
ClientType::TvHtml5ScreenEmbed => {
|
||||
name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
|
||||
|
||||
Reference in New Issue
Block a user