Initial commit

Based on JMusicBot by jagrosh (https://github.com/jagrosh/MusicBot),
via the maintained fork by arif-banai (https://github.com/arif-banai/MusicBot).
See NOTICE for attribution. Licensed under Apache 2.0.

YouTube-side changes had broken playback entirely. Fixes:

- Apply the remote signature cipher independently of OAuth. Deciphering and
  authentication are unrelated, but the cipher was only configured when OAuth
  was enabled, so the default install fell back to the bundled extractor and
  failed with "Must find sig function from script: /s/player/<id>/...".
  Now always applied, and configurable via playback.youtube.remoteCipher.url.

- Add the ANDROID client and order the client list by reliability. ANDROID is
  currently the only client YouTube reliably streams to anonymously. WEB moves
  last, as YouTube answers it with SABR-only responses carrying no playable
  format ("No supported audio streams available, available types:").

- Detect sign-in failures across the whole exception graph. The existing hint
  could never fire: it matched the aggregate AllClientsFailedException message
  rather than the per-client reasons, and compared against an ASCII apostrophe
  while YouTube sends U+2019.

- Track youtube-source past 1.18.2, which predates the TV client User-Agent fix
  and the contentLength recovery fix.

Measured on previously failing tracks: 5/5 playing, stable across repeat runs
(was 1/5). No OAuth required. Adds 17 tests; 676 unit + 48 integration pass.
This commit is contained in:
purnell569 committed 2026-09-11 13:47:20 +09:30
commit 0941987234
295 files changed
+58207

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
name: Build and Test
# Build and test on PRs and master pushes
# This workflow does NOT publish Docker images - that's handled by docker-publish.yml
on:
pull_request:
branches:
- master # PRs targeting trunk
push:
branches:
- master # Direct pushes to master
paths-ignore:
- '.github/**' # Ignore GitHub config changes (workflows, actions, templates, etc.)
- 'README.md'
- 'LICENSE'
- 'CNAME'
- 'docs/**'
- '*.example.yml'
- '*.example.yaml'
- '.circleci/**' # If not using CircleCI, ignore its config
# Cancel in-progress runs for the same workflow and branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read # Only need to checkout repository
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Java Environment
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Build and Test with Coverage
run: |
COMMIT_TIME=$(git log -1 --format=%cI)
mvn --batch-mode verify -Pcoverage -Dproject.build.outputTimestamp=$COMMIT_TIME
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v5
with:
report_type: coverage
files: target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
+120
View File
@@ -0,0 +1,120 @@
name: Publish Docker Image
# Docker publishing workflow - gated by successful build-and-test completion
# This ensures images are only published after tests pass on master
on:
# Triggered after successful build-and-test on master
# Note: The branches filter checks workflow_run.head_branch, which is the source branch
# of the triggering workflow. For PRs targeting master, head_branch is the PR source
# branch (not master), so this filter already excludes PR-triggered builds. Only direct
# pushes to master will have head_branch == 'master' and trigger this workflow.
workflow_run:
workflows: ["Build and Test"]
types:
- completed
branches:
- master
# Also trigger on version tags (v*) for releases
push:
tags:
- 'v*'
# Cancel in-progress runs for the same workflow and ref
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: arif-banai/musicbot
jobs:
publish:
name: Publish Docker Image
runs-on: ubuntu-latest
timeout-minutes: 45
# Only run if the triggering workflow succeeded (for workflow_run events)
# For tag pushes, always run
if: |
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') ||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'))
permissions:
contents: read
packages: write # Required for pushing to GHCR
steps:
- name: Checkout repository
uses: actions/checkout@v6
# For workflow_run events, checkout the commit that triggered the original workflow
with:
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
fetch-depth: 0 # Full history needed for git commands
- name: Extract metadata
id: metadata
run: |
# Extract project version from pom.xml
POM_VERSION=$(grep -m 1 '<version>' pom.xml | sed 's/.*<version>\([^<]*\)<\/version>.*/\1/' | tr -d ' ')
SHA_SHORT=$(git rev-parse --short HEAD)
TIMESTAMP=$(git log -1 --format=%cI)
# Determine if this is a tag build or master branch build
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_type }}" == "tag" ]]; then
# For tag builds, use the git tag version (strip 'v' prefix)
VERSION="${GITHUB_REF#refs/tags/v}"
IS_TAG_BUILD="true"
else
# For master builds, use pom.xml version
VERSION="$POM_VERSION"
IS_TAG_BUILD="false"
fi
# Check if version is a SNAPSHOT
if [[ "$VERSION" =~ -SNAPSHOT$ ]]; then
IS_SNAPSHOT="true"
else
IS_SNAPSHOT="false"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "pom_version=$POM_VERSION" >> "$GITHUB_OUTPUT"
echo "sha=$SHA_SHORT" >> "$GITHUB_OUTPUT"
echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT"
echo "is_tag_build=$IS_TAG_BUILD" >> "$GITHUB_OUTPUT"
echo "is_snapshot=$IS_SNAPSHOT" >> "$GITHUB_OUTPUT"
echo "Extracted version: $VERSION (pom: $POM_VERSION), SHA: $SHA_SHORT, is_tag: $IS_TAG_BUILD, is_snapshot: $IS_SNAPSHOT"
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
env:
# For multi-arch images, annotations must be attached to the image index
# GitHub Packages reads annotations from the index level for multi-arch images
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Tag rules:
# - For tag builds: version tag (from git tag) + sha tag
# - For master builds: latest + sha + version (if not SNAPSHOT)
tags: |
type=raw,value=${{ steps.metadata.outputs.version }},enable=${{ steps.metadata.outputs.is_tag_build == 'true' }}
type=raw,value=latest,enable=${{ steps.metadata.outputs.is_tag_build == 'false' }}
type=raw,value=${{ steps.metadata.outputs.version }},enable=${{ steps.metadata.outputs.is_tag_build == 'false' && steps.metadata.outputs.is_snapshot == 'false' }}
type=raw,value=sha-${{ steps.metadata.outputs.sha }}
# Labels are automatically generated from git context by metadata-action
# No need to specify them - metadata-action will create standard OCI labels
- name: Build and push Docker image
id: build
uses: ./.github/actions/docker-build
with:
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
timestamp: ${{ steps.metadata.outputs.timestamp }}
registry: ${{ env.REGISTRY }}
image-name: ${{ env.IMAGE_NAME }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Image digest
run: echo "Image pushed with digest ${{ steps.build.outputs.digest }}"
+153
View File
@@ -0,0 +1,153 @@
name: Make Release
# Manual workflow to create a GitHub release
# This sets the version, builds the JAR, and creates a release with tag
# The tag will trigger docker-publish.yml to build and push the Docker image
on:
workflow_dispatch:
inputs:
version_number:
description: 'Version Number (e.g., 0.6.3)'
required: true
type: string
info:
description: 'Description of this Release'
required: true
type: string
is_prerelease:
description: 'Mark this as a pre-release (for testing)'
required: false
type: boolean
default: false
ref:
description: 'Branch, tag, or SHA to build from (defaults to the branch this workflow is run from)'
required: false
type: string
default: ''
# Prevent concurrent releases
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
build_jar:
name: Build JAR
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write # Required to push version commit
outputs:
release_sha: ${{ steps.set-release-sha.outputs.sha }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.ref || github.ref }}
fetch-depth: 0 # Full history needed for git commands
- name: Validate branch for regular releases
if: ${{ github.event.inputs.is_prerelease != 'true' }}
run: |
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$BRANCH" != "master" ]; then
echo "❌ Error: Regular releases can only be created from the 'master' branch."
echo " Current branch: $BRANCH"
echo " To create a pre-release from this branch, check the 'Mark this as a pre-release' option."
exit 1
fi
echo "✓ Branch validation passed: $BRANCH"
- name: Set up Java Environment
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Get initial SHA (for pre-releases)
id: get-initial-sha
if: ${{ github.event.inputs.is_prerelease == 'true' }}
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set Version
run: |
mvn -B versions:set -DnewVersion=${{ github.event.inputs.version_number }}
mvn -B versions:commit
- name: Commit version bump
if: ${{ github.event.inputs.is_prerelease != 'true' }}
id: commit-version
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Stage whatever the version plugin changed
git add pom.xml
# If nothing is staged, don't fail the workflow
if git diff --cached --quiet; then
echo "No version changes to commit (pom.xml already at ${{ github.event.inputs.version_number }})."
exit 0
fi
git commit -m "chore: bump version to ${{ github.event.inputs.version_number }}"
git push
- name: Set release SHA output
id: set-release-sha
run: |
if [ "${{ github.event.inputs.is_prerelease }}" == "true" ]; then
echo "sha=${{ steps.get-initial-sha.outputs.sha }}" >> "$GITHUB_OUTPUT"
else
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
fi
- name: Build with Maven
run: |
COMMIT_TIME=$(git log -1 --format=%cI)
mvn --batch-mode verify -Dproject.build.outputTimestamp=$COMMIT_TIME
- name: Rename JAR
run: mv target/*-All.jar JMusicBot-${{ github.event.inputs.version_number }}.jar
- name: Upload JAR artifact
uses: actions/upload-artifact@v6
with:
name: jar
path: JMusicBot-${{ github.event.inputs.version_number }}.jar
if-no-files-found: error
create_release:
name: Create GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 10
needs: build_jar
permissions:
contents: write # Required to create releases and tags
steps:
- name: Download JAR artifact
uses: actions/download-artifact@v7
with:
name: jar
path: .
- name: Create Release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: v${{ github.event.inputs.version_number }}
commit: ${{ needs.build_jar.outputs.release_sha }}
name: v${{ github.event.inputs.version_number }}
draft: true
prerelease: ${{ github.event.inputs.is_prerelease }}
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/arif-banai/MusicBot/releases/download/v${{ github.event.inputs.version_number }}/JMusicBot-${{ github.event.inputs.version_number }}.jar)
+131
View File
@@ -0,0 +1,131 @@
name: Publish Preview Image
# Optional workflow for manually publishing preview Docker images from any branch/tag/SHA
# This is separate from the main CI/CD pipeline and allows explicit preview builds
on:
workflow_dispatch:
inputs:
ref:
description: 'Git ref to build (branch name, tag, or SHA)'
required: true
type: string
tag:
description: 'Optional tag suffix override (if empty, derived from ref)'
required: false
type: string
run_tests:
description: 'Run Maven tests before building Docker image'
required: false
type: boolean
default: true
# Cancel in-progress runs for the same workflow and ref
concurrency:
group: ${{ github.workflow }}-${{ github.event.inputs.ref || github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: arif-banai/musicbot
jobs:
publish-preview:
name: Publish Preview Docker Image
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
packages: write # Required for pushing to GHCR
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0 # Full history needed for git commands
- name: Extract metadata
id: metadata
run: |
# Extract project version from pom.xml
VERSION=$(grep -m 1 '<version>' pom.xml | sed 's/.*<version>\([^<]*\)<\/version>.*/\1/' | tr -d ' ')
SHA_SHORT=$(git rev-parse --short HEAD)
TIMESTAMP=$(git log -1 --format=%cI)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "sha=$SHA_SHORT" >> "$GITHUB_OUTPUT"
echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT"
echo "Extracted version: $VERSION, SHA: $SHA_SHORT"
- name: Set up Java Environment
# Run if run_tests is true (default) or explicitly set to true
if: ${{ github.event.inputs.run_tests != 'false' }}
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Run Maven tests
# Run if run_tests is true (default) or explicitly set to true
if: ${{ github.event.inputs.run_tests != 'false' }}
run: |
COMMIT_TIME=$(git log -1 --format=%cI)
mvn --batch-mode verify -Pdocker -Dproject.build.outputTimestamp=$COMMIT_TIME
- name: Sanitize tag suffix
id: sanitize
run: |
# Determine the tag suffix: use input.tag if provided, otherwise derive from ref
if [[ -n "${{ github.event.inputs.tag }}" ]]; then
TAG_SUFFIX="${{ github.event.inputs.tag }}"
else
# Derive from ref: strip refs/heads/, refs/tags/, refs/ prefixes
REF="${{ github.event.inputs.ref }}"
REF="${REF#refs/heads/}"
REF="${REF#refs/tags/}"
REF="${REF#refs/}"
TAG_SUFFIX="$REF"
fi
# Sanitize: lowercase, replace '/' with '-', strip invalid chars to [a-z0-9._-]
# Truncate to 50 characters to avoid tag length issues
SANITIZED=$(echo "$TAG_SUFFIX" | tr '[:upper:]' '[:lower:]' | tr '/' '-' | tr -cd '[:alnum:]._-' | cut -c1-50)
echo "suffix=$SANITIZED" >> "$GITHUB_OUTPUT"
echo "Sanitized tag suffix: $SANITIZED"
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
env:
# For multi-arch images, annotations must be attached to the image index
# GitHub Packages reads annotations from the index level for multi-arch images
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Tag rules for preview images:
# - preview-<sanitized-ref> tag
# - sha-<short> immutable tag
tags: |
type=raw,value=preview-${{ steps.sanitize.outputs.suffix }}
type=raw,value=sha-${{ steps.metadata.outputs.sha }}
# Labels are automatically generated from git context by metadata-action
# No need to specify them - metadata-action will create standard OCI labels
- name: Build and push Docker image
id: build
uses: ./.github/actions/docker-build
with:
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
timestamp: ${{ steps.metadata.outputs.timestamp }}
registry: ${{ env.REGISTRY }}
image-name: ${{ env.IMAGE_NAME }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Print final tags
run: |
echo "Successfully published preview Docker image"
echo "Tags: ${{ steps.meta.outputs.tags }}"
echo "Image digest: ${{ steps.build.outputs.digest }}"
@@ -0,0 +1,69 @@
name: Validate Branch Naming
# Validates branch naming conventions for feature/fix/chore/deps/release branches
# Runs only on pull requests
on:
pull_request:
types: [opened]
# Cancel in-progress runs for the same PR
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
validate-branch-name:
name: Validate Branch Naming Convention
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validate branch name
run: |
# Extract branch name from pull request head ref
BRANCH_NAME="${{ github.head_ref }}"
echo "Validating branch: $BRANCH_NAME"
# Check each allowed pattern
VALID=false
# feature/<slug>
if [[ "$BRANCH_NAME" =~ ^feature/[a-z0-9-]+$ ]]; then
VALID=true
echo "✓ Branch name matches pattern: feature/<slug>"
# fix/<slug>
elif [[ "$BRANCH_NAME" =~ ^fix/[a-z0-9-]+$ ]]; then
VALID=true
echo "✓ Branch name matches pattern: fix/<slug>"
# chore/<slug>
elif [[ "$BRANCH_NAME" =~ ^chore/[a-z0-9-]+$ ]]; then
VALID=true
echo "✓ Branch name matches pattern: chore/<slug>"
# deps/<slug>
elif [[ "$BRANCH_NAME" =~ ^deps/[a-z0-9-]+$ ]]; then
VALID=true
echo "✓ Branch name matches pattern: deps/<slug>"
# release/<version> (e.g., release/0.6.3 or release/0.6.3-rc1)
elif [[ "$BRANCH_NAME" =~ ^release/[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9-]+)?$ ]]; then
VALID=true
echo "✓ Branch name matches pattern: release/<version>"
fi
if [[ "$VALID" == "false" ]]; then
echo "❌ Branch name '$BRANCH_NAME' does not match allowed patterns:"
echo ""
echo "Allowed branch patterns:"
echo " - feature/<slug> (e.g., feature/new-player-ui)"
echo " - fix/<slug> (e.g., fix/youtube-oauth)"
echo " - chore/<slug> (e.g., chore/update-dependencies)"
echo " - deps/<slug> (e.g., deps/youtube-source-pr195)"
echo " - release/<version> (e.g., release/0.6.3 or release/0.6.3-rc1)"
echo ""
echo "Branch names should:"
echo " - Use lowercase letters, numbers, and hyphens only"
echo " - Follow the pattern: <type>/<descriptive-slug>"
echo ""
exit 1
fi
echo "✅ Branch name is valid!"