Skip to content

F1TV UHD Patch

F1TV UHD Patch #275

Workflow file for this run

name: F1TV UHD Patch
on:
schedule:
- cron: "0 */3 * * *"
workflow_dispatch:
inputs:
apkm_url:
description: "Direct URL to .apkm file (bypasses auto-download)"
required: false
type: string
force:
description: "Force rebuild even if release already exists"
required: false
type: boolean
default: false
permissions:
contents: write
env:
APKTOOL_VERSION: "2.10.0"
APKEEP_VERSION: "0.18.0"
APKEEP_CUSTOM_TAG: "v0.18.1-shield"
PLAYWRIGHT_BROWSERS_PATH: /home/runner/.cache/ms-playwright
jobs:
check:
name: Check for new version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
version_short: ${{ steps.version.outputs.version_short }}
should_build: ${{ steps.decide.outputs.should_build }}
steps:
- uses: actions/checkout@v4
- name: Restore apkeep cache
id: apkeep-cache
uses: actions/cache/restore@v4
with:
path: /usr/local/bin/apkeep
key: apkeep-v2-${{ env.APKEEP_VERSION }}
- name: Install apkeep
if: steps.apkeep-cache.outputs.cache-hit != 'true'
run: |
curl -sSL "https://github.com/EFForg/apkeep/releases/download/${APKEEP_VERSION}/apkeep-x86_64-unknown-linux-gnu" -o /usr/local/bin/apkeep
chmod +x /usr/local/bin/apkeep
- name: Check latest TV version from APKPure
id: version
run: |
echo "Listing available versions from APKPure..."
apkeep -l -a com.formulaone.production -d apk-pure 2>&1 | tee /tmp/apkeep_versions.txt
# Find the latest -tv version (last match = most recent)
TV_VERSION=$(grep -oP '[\d.]+-\S*-tv' /tmp/apkeep_versions.txt | tail -1)
if [[ -z "${TV_VERSION}" ]]; then
echo "::warning::No TV variant found in APKPure version list"
echo "version=" >> "$GITHUB_OUTPUT"
echo "version_short=" >> "$GITHUB_OUTPUT"
exit 0
fi
# Short version for the release tag (e.g. "3.0.47.1")
VERSION_SHORT=$(echo "${TV_VERSION}" | grep -oP '^[\d]+(?:\.[\d]+)+')
echo "version=${TV_VERSION}" >> "$GITHUB_OUTPUT"
echo "version_short=${VERSION_SHORT}" >> "$GITHUB_OUTPUT"
echo "Latest TV version: ${TV_VERSION} (short: ${VERSION_SHORT})"
- name: Check if release already exists
id: existing
if: steps.version.outputs.version_short != ''
run: |
if gh release view "v${{ steps.version.outputs.version_short }}" &>/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Decide whether to build
id: decide
run: |
if [[ -z "${{ steps.version.outputs.version_short }}" ]]; then
echo "should_build=false" >> "$GITHUB_OUTPUT"
echo "No TV version found, skipping"
elif [[ "${{ inputs.force }}" == "true" ]]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
echo "Forced build requested"
elif [[ -n "${{ inputs.apkm_url }}" ]]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
echo "Manual APKM URL provided"
elif [[ "${{ steps.existing.outputs.exists }}" == "false" ]]; then
echo "should_build=true" >> "$GITHUB_OUTPUT"
echo "New version: v${{ steps.version.outputs.version_short }}"
else
echo "should_build=false" >> "$GITHUB_OUTPUT"
echo "Release v${{ steps.version.outputs.version_short }} already exists, skipping"
fi
# Notify on new version detected
- name: Notify via Pushover (new version)
if: steps.decide.outputs.should_build == 'true' && !inputs.force
run: |
curl -s --form-string "token=$PUSHOVER_APP_TOKEN" \
--form-string "user=$PUSHOVER_USER_KEY" \
--form-string "title=F1TV UHD: New version detected" \
--form-string "message=Version ${{ steps.version.outputs.version }} found. Patching started." \
--form-string "priority=0" \
--form-string "sound=pushover" \
https://api.pushover.net/1/messages.json || true
env:
PUSHOVER_APP_TOKEN: ${{ secrets.PUSHOVER_APP_TOKEN }}
PUSHOVER_USER_KEY: ${{ secrets.PUSHOVER_USER_KEY }}
- name: Save apkeep cache
if: always() && steps.apkeep-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: /usr/local/bin/apkeep
key: apkeep-v2-${{ env.APKEEP_VERSION }}
patch:
name: Download, patch & release
needs: check
if: needs.check.outputs.should_build == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install Python dependencies
run: pip install -r requirements.txt
- name: Get Playwright version
id: pw
run: echo "version=$(playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
- name: Restore Playwright cache
id: pw-cache
uses: actions/cache/restore@v4
with:
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
key: playwright-${{ steps.pw.outputs.version }}
- name: Install Playwright browser
if: steps.pw-cache.outputs.cache-hit != 'true'
run: playwright install chromium --with-deps
- name: Install Playwright OS deps (from cache)
if: steps.pw-cache.outputs.cache-hit == 'true'
run: playwright install-deps chromium
- name: Restore apktool cache
id: apktool-cache
uses: actions/cache/restore@v4
with:
path: /usr/local/bin/apktool*
key: apktool-${{ env.APKTOOL_VERSION }}
- name: Install apktool
if: steps.apktool-cache.outputs.cache-hit != 'true'
run: |
curl -sSL "https://github.com/iBotPeaches/Apktool/releases/download/v${APKTOOL_VERSION}/apktool_${APKTOOL_VERSION}.jar" -o /usr/local/bin/apktool.jar
cat > /usr/local/bin/apktool << 'WRAPPER'
#!/bin/bash
java -jar /usr/local/bin/apktool.jar "$@"
WRAPPER
chmod +x /usr/local/bin/apktool
- name: Set up Android SDK tools
run: |
BT_DIR="$(ls -d ${ANDROID_HOME}/build-tools/*/ 2>/dev/null | sort -V | tail -1)"
if [[ -z "${BT_DIR}" ]]; then
echo "Installing Android build-tools..."
${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager "build-tools;35.0.0"
BT_DIR="${ANDROID_HOME}/build-tools/35.0.0/"
fi
echo "${BT_DIR}" >> "$GITHUB_PATH"
echo "Using build-tools: ${BT_DIR}"
- name: Restore custom apkeep cache
if: ${{ !inputs.apkm_url }}
id: apkeep-custom-cache
uses: actions/cache/restore@v4
with:
path: /usr/local/bin/apkeep
key: apkeep-custom-${{ env.APKEEP_CUSTOM_TAG }}
- name: Install custom apkeep (with Shield TV profile)
if: ${{ !inputs.apkm_url && steps.apkeep-custom-cache.outputs.cache-hit != 'true' }}
run: |
curl -sSL "https://github.com/Alexvbp/apkeep/releases/download/${APKEEP_CUSTOM_TAG}/apkeep-x86_64-linux.tar.gz" | tar xz -C /usr/local/bin/
chmod +x /usr/local/bin/apkeep
# ── Option A: Download from Google Play (arm64 via Shield profile) ──
- name: Download from Google Play
if: ${{ !inputs.apkm_url && env.GOOGLE_EMAIL != '' }}
id: download_gplay
run: |
mkdir -p download
echo "Downloading F1TV from Google Play (NVIDIA Shield TV profile)..."
apkeep -d google-play \
-e "${GOOGLE_EMAIL}" \
-t "${GOOGLE_AAS_TOKEN}" \
-o "device=nvidia_shield_tv,split_apk=true" \
--accept-tos \
-a "com.formulaone.production" \
download/
# split_apk=true creates a directory of APKs
SPLIT_DIR="download/com.formulaone.production"
if [[ -d "${SPLIT_DIR}" ]]; then
echo "Split APKs from Google Play:"
ls -la "${SPLIT_DIR}/"
# Wrap into APKM bundle for patch.sh
(cd "${SPLIT_DIR}" && zip -q ../f1tv-gplay.apkm *.apk)
echo "apkm_path=download/f1tv-gplay.apkm" >> "$GITHUB_OUTPUT"
echo "source=google-play" >> "$GITHUB_OUTPUT"
else
# Single APK fallback
DOWNLOADED=$(find download/ -maxdepth 1 -name 'com.formulaone.production*' -print -quit)
if [[ -n "${DOWNLOADED}" ]]; then
echo "Downloaded: ${DOWNLOADED}"
if [[ "${DOWNLOADED}" == *.apk ]]; then
mkdir -p download/bundle_gp
cp "${DOWNLOADED}" download/bundle_gp/base.apk
(cd download/bundle_gp && zip -q ../f1tv-gplay.apkm base.apk)
echo "apkm_path=download/f1tv-gplay.apkm" >> "$GITHUB_OUTPUT"
else
echo "apkm_path=${DOWNLOADED}" >> "$GITHUB_OUTPUT"
fi
echo "source=google-play" >> "$GITHUB_OUTPUT"
else
echo "::warning::Google Play download produced no files"
exit 1
fi
fi
continue-on-error: true
env:
GOOGLE_EMAIL: ${{ secrets.GOOGLE_EMAIL }}
GOOGLE_AAS_TOKEN: ${{ secrets.GOOGLE_AAS_TOKEN }}
# ── Download armeabi-v7a split (for 32-bit device compatibility) ──
- name: Download armeabi-v7a split
if: ${{ steps.download_gplay.outcome == 'success' && env.GOOGLE_EMAIL != '' }}
id: download_armv7
run: |
echo "Downloading armeabi-v7a split from Google Play (32-bit profile)..."
mkdir -p download_armv7
apkeep -d google-play \
-e "${GOOGLE_EMAIL}" \
-t "${GOOGLE_AAS_TOKEN}" \
-o "device=generic_armv7_tv,split_apk=true" \
--accept-tos \
-a "com.formulaone.production" \
download_armv7/
# Google Play names splits as com.formulaone.production.config.<abi>.apk
ARMV7_SPLIT="$(find download_armv7/ -name '*armeabi_v7a*' -o -name '*armeabi-v7a*' | head -1)"
if [[ -z "${ARMV7_SPLIT}" ]]; then
ARMV7_SPLIT="$(find download_armv7/com.formulaone.production/ \( -name '*armeabi_v7a*' -o -name '*armeabi-v7a*' \) -print -quit 2>/dev/null)"
fi
if [[ -n "${ARMV7_SPLIT}" ]]; then
echo "Found armeabi-v7a split: ${ARMV7_SPLIT}"
# Copy into the main bundle directory
MAIN_SPLIT_DIR="download/com.formulaone.production"
if [[ -d "${MAIN_SPLIT_DIR}" ]]; then
cp "${ARMV7_SPLIT}" "${MAIN_SPLIT_DIR}/"
echo "Merged armeabi-v7a split into main bundle"
# Rebuild the APKM with the extra split
(cd "${MAIN_SPLIT_DIR}" && zip -q ../f1tv-gplay.apkm *.apk)
echo "Rebuilt APKM bundle with armeabi-v7a"
else
echo "::warning::Main split directory not found, cannot merge armeabi-v7a split"
fi
else
echo "::warning::armeabi-v7a split not found in 32-bit download"
echo "Download contents:"
find download_armv7/ -name '*.apk' -ls
fi
continue-on-error: true
env:
GOOGLE_EMAIL: ${{ secrets.GOOGLE_EMAIL }}
GOOGLE_AAS_TOKEN: ${{ secrets.GOOGLE_AAS_TOKEN }}
# ── Option B: Download from APKPure via apkeep ──
- name: Download from APKPure
if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' }}
id: download_apkpure
run: |
echo "Google Play unavailable, falling back to APKPure..."
mkdir -p download
TV_VERSION="${{ needs.check.outputs.version }}"
echo "Downloading F1TV TV version: ${TV_VERSION}"
apkeep -a "com.formulaone.production@${TV_VERSION}" -d apk-pure download/
# Find the downloaded file
echo "Download directory contents:"
ls -la download/
DOWNLOADED=$(find download/ -maxdepth 1 -name 'com.formulaone.production*' -print -quit)
if [[ -z "${DOWNLOADED}" ]]; then
echo "::error::APKPure download failed"
exit 1
fi
echo "Downloaded: ${DOWNLOADED}"
# XAPK is a zip of split APKs (same as APKM)
if [[ "${DOWNLOADED}" == *.xapk ]]; then
mv "${DOWNLOADED}" "${DOWNLOADED%.xapk}.apkm"
DOWNLOADED="${DOWNLOADED%.xapk}.apkm"
fi
# Single APK: wrap it into an APKM bundle so patch.sh can handle it
if [[ "${DOWNLOADED}" == *.apk ]]; then
echo "Single APK detected, wrapping as APKM bundle..."
mkdir -p download/bundle
cp "${DOWNLOADED}" download/bundle/base.apk
(cd download/bundle && zip -q ../f1tv-apkpure.apkm base.apk)
DOWNLOADED="download/f1tv-apkpure.apkm"
fi
echo "apkm_path=${DOWNLOADED}" >> "$GITHUB_OUTPUT"
echo "source=apkpure" >> "$GITHUB_OUTPUT"
continue-on-error: true
# ── Option C: Download from APKMirror via Playwright ──
- name: Check latest version from RSS
if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' && steps.download_apkpure.outcome != 'success' }}
id: rss
run: python scripts/check_version.py
continue-on-error: true
- name: Download APKM from APKMirror
if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' && steps.download_apkpure.outcome != 'success' && steps.rss.outputs.release_url != '' }}
id: download_auto
run: |
echo "APKPure download failed, trying APKMirror..."
mkdir -p download
APKM_PATH=$(python scripts/download_apkm.py \
"${{ steps.rss.outputs.release_url }}" \
--variant-url "${{ steps.rss.outputs.variant_url }}" \
-o download)
echo "apkm_path=${APKM_PATH}" >> "$GITHUB_OUTPUT"
echo "source=apkmirror" >> "$GITHUB_OUTPUT"
continue-on-error: true
- name: Upload download debug screenshots
if: ${{ !inputs.apkm_url && steps.download_gplay.outcome != 'success' && steps.download_apkpure.outcome != 'success' && steps.download_auto.outcome != 'success' }}
uses: actions/upload-artifact@v4
with:
name: download-debug-screenshots
path: download/debug_*
retention-days: 7
if-no-files-found: ignore
# ── Option D: Download from direct URL ──
- name: Download APKM from URL
if: ${{ inputs.apkm_url }}
id: download_url
run: |
mkdir -p download
curl -sSL -o download/f1tv.apkm "${{ inputs.apkm_url }}"
echo "apkm_path=download/f1tv.apkm" >> "$GITHUB_OUTPUT"
- name: Resolve APKM path
id: apkm
run: |
if [[ -n "${{ steps.download_url.outputs.apkm_path }}" ]]; then
echo "path=${{ steps.download_url.outputs.apkm_path }}" >> "$GITHUB_OUTPUT"
elif [[ -n "${{ steps.download_gplay.outputs.apkm_path }}" ]]; then
echo "path=${{ steps.download_gplay.outputs.apkm_path }}" >> "$GITHUB_OUTPUT"
echo "source=${{ steps.download_gplay.outputs.source }}" >> "$GITHUB_OUTPUT"
elif [[ -n "${{ steps.download_apkpure.outputs.apkm_path }}" ]]; then
echo "path=${{ steps.download_apkpure.outputs.apkm_path }}" >> "$GITHUB_OUTPUT"
echo "source=${{ steps.download_apkpure.outputs.source }}" >> "$GITHUB_OUTPUT"
elif [[ -n "${{ steps.download_auto.outputs.apkm_path }}" ]]; then
echo "path=${{ steps.download_auto.outputs.apkm_path }}" >> "$GITHUB_OUTPUT"
echo "source=${{ steps.download_auto.outputs.source }}" >> "$GITHUB_OUTPUT"
else
echo "::error::All download methods failed (Google Play + APKPure + APKMirror)."
echo "::error::Trigger the workflow manually with a direct APKM URL."
exit 1
fi
# ── Decode persistent keystore from secret (optional) ──
- name: Set up keystore
if: ${{ env.KEYSTORE_B64 != '' }}
run: |
echo "${KEYSTORE_B64}" | base64 -d > /tmp/patch.keystore
echo "KEYSTORE_PATH=/tmp/patch.keystore" >> "$GITHUB_ENV"
env:
KEYSTORE_B64: ${{ secrets.KEYSTORE_B64 }}
# ── Patch ──
- name: Patch APKM
run: |
chmod +x scripts/patch.sh
bash scripts/patch.sh "${{ steps.apkm.outputs.path }}" output/
env:
KEYSTORE_PATH: ${{ env.KEYSTORE_PATH || '' }}
KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS || 'android' }}
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS || 'f1tvpatch' }}
# ── Upload artifacts (always, even if release creation fails) ──
- name: Upload patched APKs
uses: actions/upload-artifact@v4
with:
name: f1tv-uhd-patched-${{ needs.check.outputs.version_short }}
path: output/
retention-days: 90
# ── Create GitHub Release ──
- name: Create release
id: release
run: |
TAG="v${{ needs.check.outputs.version_short }}"
TITLE="F1TV UHD Patched - ${{ needs.check.outputs.version }}"
# Delete existing release if force-rebuilding
if [[ "${{ inputs.force }}" == "true" ]]; then
gh release delete "${TAG}" --yes 2>/dev/null || true
git push --delete origin "${TAG}" 2>/dev/null || true
fi
SOURCE="${{ steps.apkm.outputs.source }}"
if [[ "${SOURCE}" == "google-play" ]]; then
if [[ "${{ steps.download_armv7.outcome }}" == "success" ]]; then
SOURCE_NOTE="Source: Google Play (arm64-v8a + armeabi-v7a)"
else
SOURCE_NOTE="Source: Google Play (arm64-v8a native)"
fi
elif [[ "${SOURCE}" == "apkpure" ]]; then
SOURCE_NOTE="Source: APKPure (armeabi-v7a)"
else
SOURCE_NOTE="Source: ${SOURCE:-manual}"
fi
gh release create "${TAG}" \
--title "${TITLE}" \
--notes "$(cat <<NOTES
Patched F1TV Android TV app with UHD/4K enabled.
**Version:** \`${{ needs.check.outputs.version }}\`
**${SOURCE_NOTE}**
## Install
Download \`f1tv-uhd-patched.apkm\` below, then use the install script:
\`\`\`bash
./scripts/install.sh f1tv-uhd-patched.apkm [device-ip:5555]
\`\`\`
See the [README](https://github.com/${{ github.repository }}#installing-on-your-android-tv) for full setup instructions.
NOTES
)" \
output/f1tv-uhd-patched.apkm
echo "release_url=https://github.com/${{ github.repository }}/releases/tag/${TAG}" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ── Notify: success ──
- name: Notify via Pushover (success)
if: success()
run: |
curl -s --form-string "token=$PUSHOVER_APP_TOKEN" \
--form-string "user=$PUSHOVER_USER_KEY" \
--form-string "title=F1TV UHD: Patch ready!" \
--form-string "message=Version ${{ needs.check.outputs.version }} patched and released." \
--form-string "url=${{ steps.release.outputs.release_url }}" \
--form-string "url_title=Download from GitHub" \
--form-string "priority=0" \
--form-string "sound=pushover" \
https://api.pushover.net/1/messages.json || true
env:
PUSHOVER_APP_TOKEN: ${{ secrets.PUSHOVER_APP_TOKEN }}
PUSHOVER_USER_KEY: ${{ secrets.PUSHOVER_USER_KEY }}
# ── Notify: failure ──
- name: Notify via Pushover (failure)
if: failure()
run: |
curl -s --form-string "token=$PUSHOVER_APP_TOKEN" \
--form-string "user=$PUSHOVER_USER_KEY" \
--form-string "title=F1TV UHD: Patch failed" \
--form-string "message=Version ${{ needs.check.outputs.version }} failed to patch. Check workflow logs." \
--form-string "url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--form-string "url_title=View workflow run" \
--form-string "priority=0" \
--form-string "sound=falling" \
https://api.pushover.net/1/messages.json || true
env:
PUSHOVER_APP_TOKEN: ${{ secrets.PUSHOVER_APP_TOKEN }}
PUSHOVER_USER_KEY: ${{ secrets.PUSHOVER_USER_KEY }}
# ── Save caches (runs even if job fails) ──
- name: Save Playwright cache
if: always() && steps.pw-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
key: playwright-${{ steps.pw.outputs.version }}
- name: Save apktool cache
if: always() && steps.apktool-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: /usr/local/bin/apktool*
key: apktool-${{ env.APKTOOL_VERSION }}
- name: Save custom apkeep cache
if: always() && steps.apkeep-custom-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: /usr/local/bin/apkeep
key: apkeep-custom-${{ env.APKEEP_CUSTOM_TAG }}