Compare commits
7
Commits
ef134be9c0
...
e5f82f1005
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f82f1005
|
||
|
|
d0de6c5db9
|
||
|
|
a0393d1bba | ||
|
|
3886ccbe65 | ||
|
|
fddde60e23 | ||
|
|
838ff69090 | ||
|
|
66cc718d30 |
No files matched your search
@@ -0,0 +1 @@
|
||||
.gitea/CODEOWNERS linguist-detectable=false
|
||||
@@ -0,0 +1,3 @@
|
||||
# Pull requests request review from the eligible staff team(s).
|
||||
# One approval from either an Admin or Developer satisfies branch protection.
|
||||
* @trueog/Admins @trueog/Developers
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at j@gro.sh. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: jagrosh
|
||||
patreon: jagrosh
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Bug Report
|
||||
description: Report a bug or unexpected behavior
|
||||
title: "[Bug Report] Title Here "
|
||||
labels: []
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: Please provide a clear and concise description of what the bug is.
|
||||
placeholder: The bug is...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Provide steps you can take to reliably reproduce this bug
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected Result
|
||||
description: What did you expect to happen when this bug occurred?
|
||||
placeholder: I expected...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: debug
|
||||
attributes:
|
||||
label: Debug Output
|
||||
description: Please run the `debug` command and paste it here or upload it as an attachment. If you cannot run the command, please provide information about your system such as operating system version, JMusicBot version, etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Info
|
||||
description: Any additional info that you think might be useful or relevant to this bug
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Before submitting your issue, please affirm the following
|
||||
options:
|
||||
- label: I have looked for information about this [within the documentation](https://jmusicbot.com/)
|
||||
required: true
|
||||
- label: I have searched for similar issues [on the issues page](https://github.com/jagrosh/MusicBot/issues?q=is%3Aissue)
|
||||
required: true
|
||||
- label: I am using an official release from [the releases page](https://github.com/jagrosh/MusicBot/releases)
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: JMusicBot Version
|
||||
description: Which version of JMusicBot are you running?
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://github.com/jagrosh/MusicBot/discussions/categories/questions
|
||||
about: For general questions, please check the Discussions
|
||||
- name: Troubleshooting
|
||||
url: https://github.com/jagrosh/MusicBot/discussions/categories/troubleshooting
|
||||
about: For troubleshooting help, please check the Discussions
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature Request] Title Here"
|
||||
labels: []
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: Please provide a clear and concise description of what the problem is.
|
||||
placeholder: A common problem users face is...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: What is your ideal solution to the problem?
|
||||
description: Describe a clear and concise description of what you want to happen.
|
||||
placeholder: My solution to the above problem is...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: procedure
|
||||
attributes:
|
||||
label: How would this feature be used?
|
||||
description: Describe the basic flow/steps of using this feature.
|
||||
placeholder: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-info
|
||||
attributes:
|
||||
label: Additional Info
|
||||
description: Any additional info that you think might be useful or relevant to this feature request
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Before submitting your issue, please affirm the following
|
||||
options:
|
||||
- label: I have checked [the documentation](https://jmusicbot.com/) to make sure this feature doesn't already exist
|
||||
required: true
|
||||
- label: I have searched for [similar feature requests](https://github.com/jagrosh/MusicBot/labels/enhancement)
|
||||
required: true
|
||||
- label: I have confirmed that my requests is **not** on the [list of things that will not be added](https://jmusicbot.com/things-that-wont-be-added)
|
||||
required: true
|
||||
- label: "I am running the latest version of the bot: "
|
||||
required: true
|
||||
@@ -1,14 +0,0 @@
|
||||
### This pull request...
|
||||
- [ ] Fixes a bug
|
||||
- [ ] Introduces a new feature
|
||||
- [ ] Improves an existing feature
|
||||
- [ ] Boosts code quality or performance
|
||||
|
||||
### Description
|
||||
Replace this with a description of the pull request
|
||||
|
||||
### Purpose
|
||||
Replace this with a description of the problem or use-case
|
||||
|
||||
### Relevant Issue(s)
|
||||
This PR closes issue #...
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Build and Test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
cache: maven
|
||||
- name: Build and Test with Maven
|
||||
run: mvn --batch-mode --update-snapshots verify
|
||||
@@ -1,70 +0,0 @@
|
||||
name: Make Release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: 'Version Number'
|
||||
required: true
|
||||
type: string
|
||||
info:
|
||||
description: 'Description of this Release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build_jar:
|
||||
name: Build Jar
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
cache: maven
|
||||
- name: Set Version
|
||||
uses: datamonsters/replace-action@v2
|
||||
with:
|
||||
files: 'pom.xml'
|
||||
replacements: 'Snapshot=${{ github.event.inputs.version_number }}'
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --update-snapshots verify
|
||||
- name: Rename jar
|
||||
run: mv target/*-All.jar JMusicBot-${{ github.event.inputs.version_number }}.jar
|
||||
- name: Upload jar
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jar
|
||||
path: JMusicBot-${{ github.event.inputs.version_number }}.jar
|
||||
if-no-files-found: error
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build_jar
|
||||
steps:
|
||||
- name: Download a Build Artifact
|
||||
uses: actions/download-artifact@v3.0.0
|
||||
with:
|
||||
name: jar
|
||||
path: .
|
||||
- name: Show Artifacts
|
||||
run: ls -R
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.event.inputs.version_number }}
|
||||
name: Version ${{ github.event.inputs.version_number }}
|
||||
draft: true
|
||||
prerelease: false
|
||||
artifacts: "*.jar"
|
||||
body: |
|
||||
${{ github.event.inputs.info }}
|
||||
|
||||
---
|
||||
### Setup
|
||||
https://jmusicbot.com/setup
|
||||
https://jmusicbot.com/config
|
||||
# Download: [JMusicBot-${{ github.event.inputs.version_number }}.jar](https://github.com/jagrosh/MusicBot/releases/download/${{ github.event.inputs.version_number }}/JMusicBot-${{ github.event.inputs.version_number }}.jar)
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
*.txt
|
||||
nb*.xml
|
||||
dockerfile
|
||||
.*
|
||||
.*!.mvn/
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
@@ -1,33 +1,31 @@
|
||||
<img align="right" src="https://i.imgur.com/zrE80HY.png" height="200" width="200">
|
||||
|
||||
# JMusicBot Fork
|
||||
# JMusicBot (TrueOG Network fork)
|
||||
|
||||
A self-hosted Discord music bot fork based on JMusicBot, updated to keep working on modern Discord and JDA versions.
|
||||
A self-hosted Discord music bot, forked by the [TrueOG Network](https://true-og.net) from [chrisb09/MusicBot](https://github.com/chrisb09/MusicBot), which is in turn a modernised fork of [jagrosh/MusicBot](https://github.com/jagrosh/MusicBot).
|
||||
|
||||
This repository keeps the bot practical to run today, while also adding fork-specific features such as status messages and playback analytics logging. Possibly more.
|
||||
The upstream fork keeps the bot working on current Discord, JDA, and YouTube; this fork keeps it running on TrueOG's infrastructure.
|
||||
|
||||
## Demo
|
||||
## TrueOG Network Fork Changes
|
||||
|
||||
Temporary invite: [discord.gg/4cp9CvjwaW](https://discord.gg/4cp9CvjwaW)
|
||||
Permanent invite: [discord.gg/hK95396uB3](https://discord.gg/hK95396uB3)
|
||||
- Maven Wrapper (`./mvnw`) so builds use a pinned Maven version instead of whatever is installed
|
||||
- Selenium bumped to 4.49.0 with the CDP v153 devtools module, so YouTube PO-token extraction works with current Chromium
|
||||
- `maven-shade-plugin` now merges `META-INF/services`, so bundled `CdpInfo` (and other SPI) registrations are no longer silently dropped from the fat jar
|
||||
- YouTube PO-token capture handles the gzip-compressed `/youtubei/v1/player` request body that YouTube web client 2.20260918+ sends, and logs a warning instead of silently timing out when the body cannot be parsed
|
||||
|
||||
## What This Fork Adds
|
||||
- Make it work again, through:
|
||||
- JDA 6 support and current dependency updates
|
||||
- DAVE voice encryption support for current Discord voice requirements
|
||||
- Optional YouTube account login support
|
||||
- Optional YouTube PO token and visitor-data support
|
||||
- New niche features, such as:
|
||||
- Optional per-channel playback status message
|
||||
- Optional JDBC/SQLite playback analytics logging
|
||||
- Basic QoL improvements, such as:
|
||||
- Docker image and container-friendly deployment path
|
||||
## Inherited From Upstream (chrisb09)
|
||||
|
||||
- JDA 6 support and current dependency updates
|
||||
- DAVE voice encryption support for current Discord voice requirements
|
||||
- Optional YouTube account login, PO token, and visitor-data support
|
||||
- Optional per-channel playback status message
|
||||
- Optional JDBC/SQLite playback analytics logging
|
||||
- Regular [lavaplayer](https://mvnrepository.com/artifact/dev.arbjerg/lavaplayer)/[lavalink](https://maven.lavalink.dev/#/releases/dev/lavalink/youtube/common) bumps
|
||||
|
||||
## Quick Links
|
||||
|
||||
- Fork changes and version notes: [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md)
|
||||
- Setup guide: [docs/setup.md](/git/MusicBot/docs/setup.md)
|
||||
- Docker usage: [docs/docker.md](/git/MusicBot/docs/docker.md)
|
||||
- Setup guide: [docs/setup.md](/docs/setup.md)
|
||||
- Upstream fork changelog: [docs/fork-changelog.md](/docs/fork-changelog.md)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -61,44 +59,40 @@ Formats:
|
||||
- AAC streams
|
||||
- Stream playlists such as M3U and PLS
|
||||
|
||||
## Setup Notes
|
||||
## Building
|
||||
|
||||
If you only need the essentials:
|
||||
```bash
|
||||
./mvnw clean package
|
||||
```
|
||||
|
||||
The runnable jar is written to `target/JMusicBot-<version>-All.jar`.
|
||||
|
||||
## Setup Notes
|
||||
|
||||
1. Create a Discord bot and get its token, see [here](https://jmusicbot.com/getting-a-bot-token/).
|
||||
2. Enable the `MESSAGE_CONTENT` intent in the Discord Developer Portal [here](https://discord.com/developers/applications) ([Wiki link](https://jda.wiki/using-jda/gateway-intents-and-member-cache-policy/)).
|
||||
3. Configure `config.txt`.
|
||||
4. Run the jar directly or use Docker.
|
||||
4. Run the jar: `java -Dnogui=true -jar target/JMusicBot-<version>-All.jar`
|
||||
|
||||
For the detailed version, see [docs/setup.md](/git/MusicBot/docs/setup.md).
|
||||
For the detailed version, see [docs/setup.md](/docs/setup.md).
|
||||
|
||||
## Docker
|
||||
## YouTube Playback
|
||||
|
||||
A Docker image is published here: [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
|
||||
YouTube requires a PO token, which the bot obtains at startup by driving a headless Chromium through Selenium. Chromium must be installed, and the bundled Selenium CDP module must match the installed Chromium major version (currently v153 — see `pom.xml`). If Chromium is upgraded past that, bump `selenium-java` and the `selenium-devtools-vNNN` exclusions in `pom.xml` and rebuild.
|
||||
|
||||
For compose examples and deployment notes, see [docs/docker.md](/git/MusicBot/docs/docker.md).
|
||||
|
||||
## YouTube Login
|
||||
|
||||
This fork supports Google account login for YouTube playback workarounds.
|
||||
|
||||
Enable:
|
||||
Google account login is also supported as a fallback:
|
||||
|
||||
```conf
|
||||
youtubeoauth2=true
|
||||
```
|
||||
|
||||
Do not use your main Google account. Use a burner or otherwise disposable account instead.
|
||||
|
||||
More details are in [docs/setup.md](/git/MusicBot/docs/setup.md).
|
||||
|
||||
If you use PO tokens or visitor data for YouTube, those details are also covered in [docs/setup.md](/git/MusicBot/docs/setup.md).
|
||||
Do not use your main Google account. Use a burner or otherwise disposable account instead. Details are in [docs/setup.md](/docs/setup.md).
|
||||
|
||||
## DAVE / Voice Encryption
|
||||
|
||||
Discord currently requires DAVE for voice on modern API versions. This fork configures `libdave-jvm` during startup and bundles native binaries for common Linux, Windows, and macOS targets.
|
||||
Discord requires DAVE for voice on modern API versions. The bot configures `libdave-jvm` during startup and bundles native binaries for common Linux, Windows, and macOS targets.
|
||||
|
||||
If you deploy on an unsupported architecture, add the matching native dependency in [pom.xml](/git/MusicBot/pom.xml). You probably need to build the library from source for your architecture, so have fun with that.
|
||||
If you deploy on an unsupported architecture, add the matching native dependency in [pom.xml](/pom.xml).
|
||||
|
||||
## Upstream And Related Work
|
||||
|
||||
@@ -107,16 +101,3 @@ This fork incorporates ideas and patches related to:
|
||||
- [PR-1670](https://github.com/jagrosh/MusicBot/pull/1670)
|
||||
- [PR-1703](https://github.com/jagrosh/MusicBot/pull/1703)
|
||||
- [PR-1772](https://github.com/jagrosh/MusicBot/pull/1772)
|
||||
|
||||
If this fork does not fit your needs, another fork you may want to inspect is [SeVile/MusicBot](https://github.com/SeVile/MusicBot).
|
||||
|
||||
## Questions And Contributions
|
||||
|
||||
If you run into issues, please include:
|
||||
|
||||
- the version you are running
|
||||
- whether you are using Docker or the jar directly
|
||||
- relevant config changes
|
||||
- relevant logs
|
||||
|
||||
Fork-specific version notes live in [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md), which should help with troubleshooting and migration as the fork evolves.
|
||||
@@ -1,95 +0,0 @@
|
||||
# Docker Guide
|
||||
|
||||
This fork supports running the bot in Docker, which is usually the easiest deployment path for a server.
|
||||
|
||||
## Image
|
||||
|
||||
Published image:
|
||||
|
||||
- [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
|
||||
|
||||
Container build sources:
|
||||
|
||||
- [github.com/chrisb09/jmb-container](https://github.com/chrisb09/jmb-container)
|
||||
|
||||
## Minimal Compose Example
|
||||
|
||||
```yaml
|
||||
version: "2.2"
|
||||
|
||||
services:
|
||||
jmusicbot:
|
||||
image: chrisb09/jmusicbot:latest
|
||||
container_name: jmusicbot
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/jmb/config
|
||||
```
|
||||
|
||||
Replace `./config` with the host path you actually want to use.
|
||||
|
||||
## Config Files
|
||||
|
||||
Your persistent bot config should live in the mounted config directory. That lets you redeploy containers without losing your bot settings.
|
||||
|
||||
Typical things to configure there:
|
||||
|
||||
- bot token
|
||||
- owner ID
|
||||
- prefix
|
||||
- YouTube login toggle
|
||||
- YouTube PO token and visitor-data values if needed
|
||||
- status message toggle
|
||||
- analytics logging settings
|
||||
|
||||
## Deploy Flow
|
||||
|
||||
A typical update flow is:
|
||||
|
||||
1. Pull or rebuild the image.
|
||||
2. Restart the container.
|
||||
3. Check logs for startup errors.
|
||||
|
||||
Useful commands:
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker-compose logs -f jmusicbot
|
||||
```
|
||||
|
||||
## Alpine / musl Note
|
||||
|
||||
This fork bundles native support for Alpine-compatible musl environments as part of its DAVE setup, so Alpine-based container deployments are supported.
|
||||
|
||||
## SQLite Logging In Docker
|
||||
|
||||
If you enable analytics logging with SQLite:
|
||||
|
||||
```conf
|
||||
dblog.enabled = true
|
||||
dblog.jdbc_url = "jdbc:sqlite:musicbot.db"
|
||||
```
|
||||
|
||||
make sure the database file lives somewhere inside your mounted persistent config or data path if you want it to survive container recreation.
|
||||
|
||||
The `stats` command and scheduled server rewind embeds read from this database, so reports only include activity logged after analytics were enabled.
|
||||
|
||||
## YouTube Token Notes In Docker
|
||||
|
||||
If you use YouTube login or PO token based configuration, keep those values in your persistent mounted config rather than baking them into the image.
|
||||
|
||||
Relevant config values include:
|
||||
|
||||
```conf
|
||||
youtubeoauth2 = true
|
||||
ytpotoken = "PO_TOKEN_HERE"
|
||||
ytvisitordata = "VISITOR_DATA_HERE"
|
||||
```
|
||||
|
||||
## Recommendation
|
||||
|
||||
For production-style use, Docker is the cleanest path if you want:
|
||||
|
||||
- reproducible runtime behavior
|
||||
- simple upgrades
|
||||
- easier separation between bot code and persistent config
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# Setup Guide
|
||||
|
||||
This guide covers the normal non-Docker setup path and the main configuration points that matter on this fork.
|
||||
This guide covers the normal setup path and the main configuration points that matter on this fork.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -61,7 +61,7 @@ ytvisitordata = "VISITOR_DATA_HERE"
|
||||
Build:
|
||||
|
||||
```bash
|
||||
mvn clean package
|
||||
./mvnw clean package
|
||||
```
|
||||
|
||||
Run:
|
||||
@@ -165,7 +165,7 @@ Personal member reports are self-only unless the requester has Manage Server. Sc
|
||||
|
||||
## Windows Note
|
||||
|
||||
If you run the bot directly on Windows rather than in Docker, install Java first and consider a service wrapper such as [NSSM](http://nssm.cc/) if you want it to run in the background.
|
||||
If you run the bot on Windows, install Java first and consider a service wrapper such as [NSSM](http://nssm.cc/) if you want it to run in the background.
|
||||
|
||||
## Related Fork Context
|
||||
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.jagrosh</groupId>
|
||||
<artifactId>JMusicBot</artifactId>
|
||||
<version>0.6.6</version>
|
||||
<version>0.6.7</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<repositories>
|
||||
@@ -151,7 +151,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.arbjerg</groupId>
|
||||
<artifactId>lavaplayer</artifactId>
|
||||
<version>2.2.4</version>
|
||||
<version>2.2.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.arbjerg</groupId>
|
||||
@@ -161,7 +161,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.lavalink.youtube</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
<version>1.18.1</version>
|
||||
<version>f45bbb7aebfcbc1c553769e04af6cd43afa8b7c3-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jagrosh</groupId>
|
||||
@@ -203,20 +203,17 @@
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>4.27.0</version>
|
||||
<version>4.49.0</version>
|
||||
<!-- https://github.com/SeleniumHQ/selenium/issues/13791 -->
|
||||
<!-- selenium-java 4.49.0 ships CDP v151/v152/v153; keep only the one matching the installed Chromium -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-devtools-v85</artifactId>
|
||||
<artifactId>selenium-devtools-v151</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-devtools-v129</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-devtools-v130</artifactId>
|
||||
<artifactId>selenium-devtools-v152</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
@@ -274,6 +271,7 @@
|
||||
</includes>
|
||||
</artifactSet>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
|
||||
<resource>reference.conf</resource>
|
||||
</transformer>
|
||||
|
||||
@@ -176,7 +176,7 @@ public class Bot
|
||||
return;
|
||||
shuttingDown = true;
|
||||
threadpool.shutdownNow();
|
||||
if(jda.getStatus()!=JDA.Status.SHUTTING_DOWN)
|
||||
if(jda != null && jda.getStatus()!=JDA.Status.SHUTTING_DOWN)
|
||||
{
|
||||
jda.getGuilds().stream().forEach(g ->
|
||||
{
|
||||
|
||||
@@ -40,7 +40,17 @@ import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
|
||||
import dev.lavalink.youtube.YoutubeAudioSourceManager;
|
||||
import dev.lavalink.youtube.clients.Android;
|
||||
import dev.lavalink.youtube.clients.AndroidMusic;
|
||||
import dev.lavalink.youtube.clients.AndroidVr;
|
||||
import dev.lavalink.youtube.clients.ClientOptions;
|
||||
import dev.lavalink.youtube.clients.Ios;
|
||||
import dev.lavalink.youtube.clients.MWeb;
|
||||
import dev.lavalink.youtube.clients.Music;
|
||||
import dev.lavalink.youtube.clients.Tv;
|
||||
import dev.lavalink.youtube.clients.TvHtml5Simply;
|
||||
import dev.lavalink.youtube.clients.Web;
|
||||
import dev.lavalink.youtube.clients.WebEmbedded;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -68,7 +78,7 @@ public class PlayerManager extends DefaultAudioPlayerManager
|
||||
{
|
||||
TransformativeAudioSourceManager.createTransforms(bot.getConfig().getTransforms()).forEach(t -> registerSourceManager(t));
|
||||
|
||||
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
|
||||
YoutubeAudioSourceManager yt = setupYoutubeAudioSourceManager();
|
||||
if (config.getYTRoutingPlanner() != YouTubeUtil.RoutingPlanner.NONE)
|
||||
{
|
||||
AbstractRoutePlanner routePlanner = YouTubeUtil.createRouterPlanner(config.getYTRoutingPlanner(), config.getYTIpBlocks());
|
||||
@@ -85,7 +95,15 @@ public class PlayerManager extends DefaultAudioPlayerManager
|
||||
registerSourceManager(SoundCloudAudioSourceManager.createDefault());
|
||||
registerSourceManager(new BandcampAudioSourceManager());
|
||||
registerSourceManager(new VimeoAudioSourceManager());
|
||||
registerSourceManager(new TwitchStreamAudioSourceManager());
|
||||
try
|
||||
{
|
||||
registerSourceManager(new TwitchStreamAudioSourceManager());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGER.warn("Skipping Twitch source initialization because Twitch lookup failed during startup: {}", e.getMessage());
|
||||
LOGGER.debug("Twitch source initialization failure:", e);
|
||||
}
|
||||
registerSourceManager(new BeamAudioSourceManager());
|
||||
registerSourceManager(new GetyarnAudioSourceManager());
|
||||
registerSourceManager(new NicoAudioSourceManager());
|
||||
@@ -98,7 +116,20 @@ public class PlayerManager extends DefaultAudioPlayerManager
|
||||
|
||||
private YoutubeAudioSourceManager setupYoutubeAudioSourceManager()
|
||||
{
|
||||
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
|
||||
ClientOptions playbackOnly = new ClientOptions();
|
||||
playbackOnly.setSearching(false);
|
||||
|
||||
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true,
|
||||
new Ios(playbackOnly),
|
||||
new Music(playbackOnly),
|
||||
new AndroidMusic(playbackOnly),
|
||||
new TvHtml5Simply(playbackOnly),
|
||||
new Tv(playbackOnly),
|
||||
new Web(),
|
||||
new MWeb(),
|
||||
new WebEmbedded(),
|
||||
new AndroidVr(playbackOnly),
|
||||
new Android(playbackOnly));
|
||||
yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages());
|
||||
|
||||
// OAuth2 setup
|
||||
|
||||
@@ -4,28 +4,41 @@ import com.sedmelluq.lava.extensions.youtuberotator.planner.*;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import dev.lavalink.youtube.clients.Web;
|
||||
import dev.lavalink.youtube.clients.WebEmbedded;
|
||||
import org.json.JSONObject;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Cookie;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriverService;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.openqa.selenium.chromium.ChromiumDriverLogLevel;
|
||||
import org.openqa.selenium.devtools.NetworkInterceptor;
|
||||
import org.openqa.selenium.logging.LogEntries;
|
||||
import org.openqa.selenium.logging.LogEntry;
|
||||
import org.openqa.selenium.logging.LogType;
|
||||
import org.openqa.selenium.logging.LoggingPreferences;
|
||||
import org.openqa.selenium.os.ExecutableFinder;
|
||||
import org.openqa.selenium.remote.http.Contents;
|
||||
import org.openqa.selenium.remote.http.Filter;
|
||||
import org.openqa.selenium.remote.http.HttpMethod;
|
||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
|
||||
@@ -105,6 +118,21 @@ public class YouTubeUtil {
|
||||
|
||||
chromeOptions.setBinary(userChromePath != null ? userChromePath : wdm.getBrowserPath().get().toString());
|
||||
|
||||
// Essential flags for headless/containerized environments (Docker)
|
||||
chromeOptions.addArguments("--no-sandbox");
|
||||
chromeOptions.addArguments("--disable-dev-shm-usage");
|
||||
chromeOptions.addArguments("--disable-gpu");
|
||||
chromeOptions.addArguments("--window-size=1920,1080");
|
||||
chromeOptions.addArguments("--disable-blink-features=AutomationControlled");
|
||||
chromeOptions.addArguments("--autoplay-policy=no-user-gesture-required");
|
||||
chromeOptions.addArguments("--disable-features=IsolateOrigins,site-per-process");
|
||||
chromeOptions.addArguments("--disable-site-isolation-trials");
|
||||
|
||||
// Enable Chrome performance logging to capture Network.requestWillBeSent events
|
||||
LoggingPreferences logPrefs = new LoggingPreferences();
|
||||
logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
|
||||
chromeOptions.setCapability("goog:loggingPrefs", logPrefs);
|
||||
|
||||
if (!headless)
|
||||
chromeOptions.addArguments("--auto-open-devtools-for-tabs");
|
||||
else
|
||||
@@ -117,77 +145,274 @@ public class YouTubeUtil {
|
||||
WebDriver driver = new ChromeDriver(chromeDriverBuilder.build(), chromeOptions);
|
||||
|
||||
//
|
||||
// 2. Setup a network interceptor to intercept the player API request & obtain po token and visitor data
|
||||
//
|
||||
|
||||
// as the network interceptor below runs on a different thread, we'll have it notify us on this/current thread
|
||||
// whether retrieving the data failed (completes with the Exception) or succeeded (completes with null)
|
||||
CompletableFuture<Exception> interceptorFuture = new CompletableFuture<>();
|
||||
|
||||
NetworkInterceptor interceptor = new NetworkInterceptor(driver,
|
||||
(Filter) next -> req -> {
|
||||
|
||||
if (req.getMethod() != HttpMethod.POST || !req.getUri().contains("/youtubei/v1/player"))
|
||||
return next.execute(req);
|
||||
|
||||
try {
|
||||
String rawBody = Contents.string(req);
|
||||
JSONObject body = new JSONObject(rawBody);
|
||||
|
||||
String visitorData = body
|
||||
.getJSONObject("context")
|
||||
.getJSONObject("client")
|
||||
.getString("visitorData");
|
||||
|
||||
String poToken = body
|
||||
.getJSONObject("serviceIntegrityDimensions")
|
||||
.getString("poToken");
|
||||
|
||||
|
||||
LOGGER.info("Successfully retrieved PO token & visitor data for YouTube playback.");
|
||||
LOGGER.debug("PO token: {}", poToken);
|
||||
LOGGER.debug("Visitor Data: {}", visitorData);
|
||||
|
||||
if (poToken.length() < 160)
|
||||
LOGGER.warn("There is a high chance that the retrieved PO token will not work. Try on a different network.");
|
||||
|
||||
Web.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
|
||||
interceptorFuture.complete(null);
|
||||
} catch (Exception e) {
|
||||
interceptorFuture.complete(e);
|
||||
}
|
||||
|
||||
return next.execute(req);
|
||||
});
|
||||
|
||||
//
|
||||
// 3. Navigate to YouTube & start the player
|
||||
// 2. Start local HTTP server to host iframe embed & extract PO token from performance logs
|
||||
//
|
||||
HttpServer server = null;
|
||||
try {
|
||||
driver.get("https://www.youtube.com/embed/aqz-KE-bpKQ");
|
||||
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15));
|
||||
WebElement moviePlayer = driver.findElement(By.cssSelector("#movie_player"));
|
||||
moviePlayer.click();
|
||||
// Start a lightweight local HTTP server to host the YouTube embed in an iframe.
|
||||
// This provides a valid origin/Referer (http://127.0.0.1:PORT) so YouTube does not
|
||||
// reject the embed with Error 153 "Video player configuration error".
|
||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/embed", exchange -> {
|
||||
String html = "<!DOCTYPE html>" +
|
||||
"<html>" +
|
||||
"<head>" +
|
||||
"<meta name=\"referrer\" content=\"strict-origin-when-cross-origin\">" +
|
||||
"<style>html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000;}" +
|
||||
"iframe{width:100%;height:100%;border:none;}</style>" +
|
||||
"</head>" +
|
||||
"<body>" +
|
||||
"<iframe id=\"player\" " +
|
||||
"src=\"https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ?autoplay=1&mute=1&enablejsapi=1\" " +
|
||||
"referrerpolicy=\"strict-origin-when-cross-origin\" " +
|
||||
"allow=\"autoplay; encrypted-media; picture-in-picture\" " +
|
||||
"allowfullscreen></iframe>" +
|
||||
"</body>" +
|
||||
"</html>";
|
||||
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
|
||||
exchange.getResponseHeaders().set("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
int serverPort = server.getAddress().getPort();
|
||||
|
||||
// First, navigate to youtube.com to inject consent cookies to bypass EU/GDPR consent walls
|
||||
try {
|
||||
// Wait for the network interceptor to tell us if it got the data
|
||||
Exception result = interceptorFuture.get(20, TimeUnit.SECONDS);
|
||||
if (result != null)
|
||||
throw new Exception("Failed to read YouTube player request body", result);
|
||||
} catch (TimeoutException e) {
|
||||
throw new Exception("Timed out waiting for YouTube player request");
|
||||
driver.get("https://www.youtube.com");
|
||||
driver.manage().addCookie(new Cookie.Builder("SOCS", "CAESEwgDEgk0ODE3Nzk3MjQaAmVuIAEaBgiA_LyaBg")
|
||||
.domain(".youtube.com")
|
||||
.path("/")
|
||||
.build());
|
||||
driver.manage().addCookie(new Cookie.Builder("CONSENT", "PENDING+999")
|
||||
.domain(".youtube.com")
|
||||
.path("/")
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Could not pre-set consent cookies: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Navigate to local embed wrapper page
|
||||
driver.get("http://127.0.0.1:" + serverPort + "/embed");
|
||||
|
||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
|
||||
// Wait for iframe and switch to it to interact with player
|
||||
try {
|
||||
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.id("player")));
|
||||
|
||||
// Dismiss any consent dialog inside the iframe if present
|
||||
try {
|
||||
List<WebElement> consentButtons = driver.findElements(By.xpath(
|
||||
"//button[contains(@aria-label, 'Agree') or contains(@aria-label, 'Accept') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'agree') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accept') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'alle akzeptieren') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ich stimme zu') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'tout accepter') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accepter tout') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accetta tutto') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'aceptar todo')]"
|
||||
));
|
||||
for (WebElement btn : consentButtons) {
|
||||
if (btn.isDisplayed()) {
|
||||
LOGGER.info("Dismissing consent dialog inside iframe...");
|
||||
btn.click();
|
||||
Thread.sleep(1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// Try to click player inside iframe to trigger playback and /player request
|
||||
try {
|
||||
WebElement moviePlayer = wait.until(ExpectedConditions.presenceOfElementLocated(
|
||||
By.cssSelector("#movie_player, .ytp-large-play-button, .html5-video-player, video")
|
||||
));
|
||||
moviePlayer.click();
|
||||
} catch (Exception ignored) {
|
||||
((JavascriptExecutor) driver).executeScript(
|
||||
"var p = document.getElementById('movie_player') || document.querySelector('video'); " +
|
||||
"if (p) { if (p.playVideo) p.playVideo(); else if (p.play) p.play(); }"
|
||||
);
|
||||
}
|
||||
|
||||
// Switch back to default context so performance logging and main window operations work
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Could not interact with embed iframe: {}", e.getMessage());
|
||||
try {
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// Poll performance logs to capture the /youtubei/v1/player request
|
||||
boolean success = false;
|
||||
long deadline = System.currentTimeMillis() + 25_000;
|
||||
long nextActionTime = System.currentTimeMillis() + 5_000;
|
||||
|
||||
while (System.currentTimeMillis() < deadline && !success) {
|
||||
LogEntries logEntries = driver.manage().logs().get(LogType.PERFORMANCE);
|
||||
for (LogEntry entry : logEntries) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(entry.getMessage());
|
||||
JSONObject message = json.optJSONObject("message");
|
||||
if (message == null) continue;
|
||||
|
||||
String method = message.optString("method");
|
||||
if ("Network.requestWillBeSent".equals(method)) {
|
||||
JSONObject params = message.optJSONObject("params");
|
||||
if (params == null) continue;
|
||||
JSONObject req = params.optJSONObject("request");
|
||||
if (req == null) continue;
|
||||
String url = req.optString("url", "");
|
||||
if (url.contains("/youtubei/v1/player")) {
|
||||
String postData = decodePostData(req);
|
||||
|
||||
if (postData == null || postData.isEmpty()) {
|
||||
LOGGER.warn("Saw YouTube player request without a readable body, waiting for another...");
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject body;
|
||||
try {
|
||||
body = new JSONObject(postData);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("YouTube player request body is not JSON ({}), waiting for another...", e.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
String visitorData = null;
|
||||
if (body.has("context") && body.getJSONObject("context").has("client")) {
|
||||
visitorData = body.getJSONObject("context").getJSONObject("client").optString("visitorData", null);
|
||||
}
|
||||
|
||||
String poToken = null;
|
||||
if (body.has("serviceIntegrityDimensions")) {
|
||||
poToken = body.getJSONObject("serviceIntegrityDimensions").optString("poToken", null);
|
||||
}
|
||||
|
||||
if (visitorData != null && poToken != null) {
|
||||
LOGGER.info("Successfully retrieved PO token & visitor data for YouTube playback.");
|
||||
LOGGER.debug("PO token: {}", poToken);
|
||||
LOGGER.debug("Visitor Data: {}", visitorData);
|
||||
|
||||
// Set PO token and visitor data on both Web and WebEmbedded clients
|
||||
Web.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
WebEmbedded.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Error parsing performance log entry: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (success) break;
|
||||
|
||||
// If still not captured after a while, try re-triggering play inside iframe
|
||||
if (System.currentTimeMillis() > nextActionTime) {
|
||||
try {
|
||||
driver.switchTo().frame("player");
|
||||
((JavascriptExecutor) driver).executeScript(
|
||||
"var p = document.getElementById('movie_player') || document.querySelector('video'); " +
|
||||
"if (p) { if (p.playVideo) p.playVideo(); else if (p.play) p.play(); }"
|
||||
);
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
nextActionTime = System.currentTimeMillis() + 5_000;
|
||||
}
|
||||
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
saveDebugPage(driver);
|
||||
throw new Exception("Timed out waiting for YouTube player request. Current URL: " + driver.getCurrentUrl());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to obtain PO token for YouTube playback: {}", e.getMessage());
|
||||
LOGGER.debug("Exception:", e);
|
||||
} finally {
|
||||
interceptor.close();
|
||||
// Only quit the browser when not in headless mode. Helps with troubleshooting.
|
||||
if (server != null) {
|
||||
try {
|
||||
server.stop(0);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
// Only quit the browser when in headless mode. Helps with troubleshooting.
|
||||
if (headless)
|
||||
driver.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the request body of a captured Network.requestWillBeSent event.
|
||||
* Prefers the raw Base64 postDataEntries (Chrome 130+) over the lossy postData string,
|
||||
* and transparently gunzips the body when the client sent it with Content-Encoding: gzip
|
||||
* (YouTube web client 2.20260918+ compresses the /youtubei/v1/player request).
|
||||
*/
|
||||
private static String decodePostData(JSONObject req) {
|
||||
byte[] raw = null;
|
||||
org.json.JSONArray entries = req.optJSONArray("postDataEntries");
|
||||
if (entries != null) {
|
||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||
for (int i = 0; i < entries.length(); i++) {
|
||||
JSONObject entryObj = entries.optJSONObject(i);
|
||||
if (entryObj != null && entryObj.has("bytes")) {
|
||||
try {
|
||||
buf.write(java.util.Base64.getDecoder().decode(entryObj.getString("bytes")));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
if (buf.size() > 0) raw = buf.toByteArray();
|
||||
}
|
||||
if (raw == null) {
|
||||
String postData = req.optString("postData", null);
|
||||
if (postData == null || postData.isEmpty()) return null;
|
||||
raw = postData.getBytes(StandardCharsets.ISO_8859_1);
|
||||
}
|
||||
|
||||
if (raw.length > 2 && (raw[0] & 0xff) == 0x1f && (raw[1] & 0xff) == 0x8b) {
|
||||
try (GZIPInputStream gz = new GZIPInputStream(new ByteArrayInputStream(raw))) {
|
||||
raw = gz.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to gunzip YouTube player request body: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return new String(raw, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void saveDebugPage(WebDriver driver) {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String currentUrl = driver.getCurrentUrl();
|
||||
sb.append("<!-- Current URL: ").append(currentUrl).append(" -->\n");
|
||||
sb.append("<!-- MAIN PAGE SOURCE -->\n");
|
||||
try {
|
||||
sb.append(driver.getPageSource()).append("\n\n");
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
try {
|
||||
driver.switchTo().frame("player");
|
||||
sb.append("<!-- IFRAME SOURCE -->\n");
|
||||
sb.append(driver.getPageSource()).append("\n");
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
File debugFile = new File("youtube_debug.html");
|
||||
try (FileWriter writer = new FileWriter(debugFile)) {
|
||||
writer.write(sb.toString());
|
||||
}
|
||||
LOGGER.info("Saved YouTube debug page source to {} (Current URL: {})", debugFile.getAbsolutePath(), currentUrl);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to save YouTube debug page source: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user