diff --git a/.eas/build/android-production-apk.yml b/.eas/build/android-production-apk.yml new file mode 100644 index 00000000..757192bb --- /dev/null +++ b/.eas/build/android-production-apk.yml @@ -0,0 +1,25 @@ +# Custom EAS Build config for Android phone APK (downloadable artifact). +# Same bun-forcing flow as android-production.yml, but builds an APK +# (assembleRelease) instead of an AAB — for sideloading / GitHub artifact. +# Referenced from eas.json: build.production-apk.android.config +build: + name: Android phone APK (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + - run: + name: Prebuild (Android, bun) + command: bunx expo prebuild --platform android --no-install + + - eas/configure_android_version + - eas/inject_android_credentials + + - eas/run_gradle: + inputs: + command: :app:assembleRelease + + - eas/find_and_upload_build_artifacts diff --git a/.eas/build/android-production-tv.yml b/.eas/build/android-production-tv.yml new file mode 100644 index 00000000..a3fc9eff --- /dev/null +++ b/.eas/build/android-production-tv.yml @@ -0,0 +1,27 @@ +# Custom EAS Build config for Android TV APK (downloadable artifact). +# Same bun-forcing flow, with EXPO_TV=1 (set via the profile env in +# eas.json) so prebuild generates the TV variant. Builds an APK for +# sideloading onto Android TV devices. +# Referenced from eas.json: build.production-apk-tv.android.config +build: + name: Android TV APK (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + # EXPO_TV=1 comes from the profile env, so prebuild targets Android TV. + - run: + name: Prebuild (Android TV, bun) + command: bunx expo prebuild --platform android --no-install + + - eas/configure_android_version + - eas/inject_android_credentials + + - eas/run_gradle: + inputs: + command: :app:assembleRelease + + - eas/find_and_upload_build_artifacts diff --git a/.eas/build/android-production.yml b/.eas/build/android-production.yml new file mode 100644 index 00000000..651ff2b6 --- /dev/null +++ b/.eas/build/android-production.yml @@ -0,0 +1,38 @@ +# Custom EAS Build config for Android (production AAB). +# +# Why this exists: EAS's managed build can't detect Bun's text lockfile +# (bun.lock) and falls back to yarn, which breaks our install. The managed +# steps `eas/install_node_modules` and `eas/prebuild` both use "the package +# manager detected based on your project", so we replace them with explicit +# `bun` commands. Everything else uses EAS's built-in functions so we still +# get remote versioning, credentials, and artifact upload. +# +# Referenced from eas.json: build.production.android.config = android-production.yml +build: + name: Android production (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + # android/ is gitignored, so generate native code fresh. --no-install + # because deps are already installed above; bunx keeps it on bun. + - run: + name: Prebuild (Android, bun) + command: bunx expo prebuild --platform android --no-install + + # Applies the EAS-resolved remote versionCode/versionName (autoIncrement + # in eas.json) into the freshly prebuilt android/ project. + - eas/configure_android_version + + # Injects the remote Android keystore / signing config. + - eas/inject_android_credentials + + # Build the Play Store app bundle (.aab). + - eas/run_gradle: + inputs: + command: :app:bundleRelease + + - eas/find_and_upload_build_artifacts diff --git a/.eas/build/ios-production.yml b/.eas/build/ios-production.yml new file mode 100644 index 00000000..f9e228f7 --- /dev/null +++ b/.eas/build/ios-production.yml @@ -0,0 +1,44 @@ +# Custom EAS Build config for iOS + tvOS (App Store), forcing bun. +# +# Shared by both the iPhone profile (production) and the tvOS profile +# (production_tv). The profile decides the rest: +# - production_tv sets EXPO_TV=1 (env) so prebuild targets tvOS, and +# credentialsSource: local (EAS can't manage tvOS creds remotely). +# - production uses remote-managed iOS credentials. +# +# Like the Android configs, this replaces eas/install_node_modules and +# eas/prebuild (both auto-detect the wrong package manager) with explicit +# bun commands, and keeps EAS built-ins for credentials/version/fastlane. +build: + name: iOS/tvOS App Store (bun) + steps: + - eas/checkout + + - run: + name: Install dependencies (bun, frozen) + command: bun install --frozen-lockfile + + - eas/resolve_apple_team_id_from_credentials: + id: resolve_team + + # android/ + ios/ are gitignored, so generate native code fresh. + # EXPO_TV (from the profile env) selects iPhone vs tvOS. --no-install + # skips JS + pod install; we install pods explicitly below with bun deps. + - run: + name: Prebuild (iOS/tvOS, bun) + command: bunx expo prebuild --platform ios --no-install + + - run: + name: Install CocoaPods + working_directory: ./ios + command: pod install + + - eas/configure_ios_credentials + - eas/configure_ios_version + + - eas/generate_gymfile_from_template: + inputs: + credentials: ${ eas.job.secrets.buildCredentials } + + - eas/run_fastlane + - eas/find_and_upload_build_artifacts diff --git a/.gitattributes b/.gitattributes index 56dea966..4d651aeb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,28 @@ -.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text +# Normalise line endings to LF for everyone. Files are stored as LF in git and +# checked out as LF on every OS, so Windows clones stop producing CRLF churn +# (no more "LF will be replaced by CRLF" warnings) regardless of core.autocrlf. +* text=auto eol=lf + +# Windows-only scripts must stay CRLF +*.bat text eol=crlf +*.cmd text eol=crlf + +# Binary assets — never touched / never normalised +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.webp binary +*.ico binary +*.icns binary +*.ttf binary +*.otf binary +*.woff binary +*.woff2 binary +*.mp3 binary +*.mp4 binary +*.mov binary +*.pdf binary +*.keystore binary +*.jks binary +*.p12 binary diff --git a/.github/ISSUE_TEMPLATE/issue_report.yml b/.github/ISSUE_TEMPLATE/issue_report.yml index ccdb0d0e..af86644d 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.yml +++ b/.github/ISSUE_TEMPLATE/issue_report.yml @@ -1,5 +1,5 @@ name: "🐛 Bug Report" -description: Create a report to help us improve +description: Create a report to help Streamyfin improve title: "[Bug]: " labels: - "🐛 bug" @@ -36,7 +36,7 @@ body: attributes: label: What happened? description: A clear and concise description of what the bug is. - placeholder: Describe what happened in detail. + placeholder: Describe what happened in detail, the more precise the better. validations: required: true @@ -67,7 +67,7 @@ body: attributes: label: Which device and operating system are you using? description: Please provide your device model and OS version - placeholder: e.g. iPhone 15 Pro, iOS 18.1.1 or Samsung Galaxy S24, Android 14 + placeholder: e.g. iPhone 17 Pro / iOS 26.5.1, Samsung Galaxy S25 / Android 16, Apple TV / tvOS 26.5 validations: required: true @@ -75,11 +75,11 @@ body: id: version attributes: label: Streamyfin Version - description: What version of Streamyfin are you running? + description: What version of Streamyfin are you using? options: - - 0.47.1 - - 0.30.2 - - older + - 0.54.1 + - 0.51.0 + - Older - TestFlight/Development build validations: required: true @@ -90,9 +90,9 @@ body: label: Jellyfin Server Information description: Please provide details about your Jellyfin server placeholder: | - - Jellyfin Server Version: e.g. 10.10.7 - - Server OS: e.g. Ubuntu 22.04, Windows 11, Docker - - Connection: e.g. Local network, Remote via domain, VPN + - Jellyfin Server Version: e.g. 10.11.10 + - Server OS: e.g. Ubuntu 26.04, Windows 11, Docker, Proxmox + - Connection: e.g. Local network, remote via domain, VPN - type: textarea id: screenshots @@ -104,7 +104,7 @@ body: id: logs attributes: label: Relevant logs (if available) - description: If you have access to app logs or crash reports, please include them here. **Remember to remove any personal information like server URLs or usernames.** + description: If you have access to app logs or crash reports, please include them here. **Remember to remove any personal information like server URL, API keys or usernames.** render: shell - type: textarea diff --git a/.github/renovate.json b/.github/renovate.json index fdbe3734..45c62042 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -44,22 +44,42 @@ ] } }, - "lockFileMaintenance": { - "vulnerabilityAlerts": { - "enabled": true, - "addLabels": ["security", "vulnerability"], - "assigneesFromCodeOwners": true, - "commitMessageSuffix": " [SECURITY]" + "vulnerabilityAlerts": { + "enabled": true, + "addLabels": ["security", "vulnerability"], + "assigneesFromCodeOwners": true, + "commitMessageSuffix": " [SECURITY]" + }, + "packageRules": [ + { + "description": "Expo SDK coherence: expo, react, react-native and Expo-managed modules are pinned by the Expo SDK and must move together (via `expo install --fix`), so do not raise individual update PRs — group them and require manual approval from the Dependency Dashboard", + "matchPackageNames": [ + "expo", + "react", + "react-dom", + "react-native", + "react-native-web", + "expo-*", + "@expo/*" + ], + "groupName": "Expo SDK", + "dependencyDashboardApproval": true }, - "packageRules": [ - { - "description": "Group minor and patch GitHub Action updates into a single PR", - "matchManagers": ["github-actions"], - "groupName": "CI dependencies", - "groupSlug": "ci-deps", - "matchUpdateTypes": ["minor", "patch", "digest", "pin"], - "automerge": true - } - ] - } + { + "description": "Group minor and patch GitHub Action updates into a single PR", + "matchManagers": ["github-actions"], + "groupName": "CI dependencies", + "groupSlug": "ci-deps", + "matchUpdateTypes": ["minor", "patch", "digest", "pin"], + "automerge": true + }, + { + "description": "androidx and other Google-hosted Maven packages resolve from Google's Maven repository (not Maven Central)", + "matchDatasources": ["maven"], + "registryUrls": [ + "https://dl.google.com/dl/android/maven2/", + "https://repo.maven.apache.org/maven2/" + ] + } + ] } diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml index fd68e23a..c3fee61b 100644 --- a/.github/workflows/build-apps.yml +++ b/.github/workflows/build-apps.yml @@ -11,6 +11,15 @@ on: push: branches: [develop, master] +# Exposed to `expo prebuild` (app.config.js → extra.build) so Settings can show the +# branch + commit + Actions run a CI build was made from. EAS cloud builds use +# EAS_BUILD_* instead. The run number maps a sideloaded build back to its Actions +# run (artifacts + logs) without needing Expo access. +env: + EXPO_PUBLIC_GIT_BRANCH: ${{ github.head_ref || github.ref_name }} + EXPO_PUBLIC_GIT_COMMIT: ${{ github.sha }} + EXPO_PUBLIC_GIT_RUN_NUMBER: ${{ github.run_number }} + jobs: build-android-phone: if: (!contains(github.event.head_commit.message, '[skip ci]')) @@ -33,7 +42,7 @@ jobs: swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -116,7 +125,7 @@ jobs: swap-storage: false - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -187,7 +196,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -219,10 +228,10 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🏗️ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} @@ -231,7 +240,9 @@ jobs: - name: 🚀 Build iOS app env: EXPO_TV: 0 - run: eas build -p ios --local --non-interactive + # `ci` profile (extends production, autoIncrement off): keeps CI builds out of + # the production version tier and stops them inflating the store build counter. + run: eas build -p ios --local --non-interactive --profile ci - name: 📅 Set date tag run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV @@ -252,7 +263,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -284,7 +295,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🚀 Build iOS app env: @@ -312,7 +323,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -344,10 +355,10 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🏗️ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} @@ -356,7 +367,7 @@ jobs: - name: 🚀 Build iOS app env: EXPO_TV: 1 - run: eas build -p ios --local --non-interactive + run: eas build -p ios --local --non-interactive --profile ci_tv - name: 📅 Set date tag run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV @@ -380,7 +391,7 @@ jobs: steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -412,7 +423,7 @@ jobs: uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1 with: # renovate: datasource=custom.xcode depName=xcode versioning=loose - xcode-version: "26.4" + xcode-version: "26.5" - name: 🚀 Build iOS app env: diff --git a/.github/workflows/check-lockfile.yml b/.github/workflows/check-lockfile.yml index ae4c0fe0..0cb8afc3 100644 --- a/.github/workflows/check-lockfile.yml +++ b/.github/workflows/check-lockfile.yml @@ -19,7 +19,7 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} show-progress: false diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml index ba1c08dc..f79cf58a 100644 --- a/.github/workflows/ci-codeql.yml +++ b/.github/workflows/ci-codeql.yml @@ -24,16 +24,16 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: 🏁 Initialize CodeQL - uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} queries: +security-extended,security-and-quality - name: 🛠️ Autobuild - uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 - name: 🧪 Perform CodeQL Analysis - uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/conflict.yml b/.github/workflows/conflict.yml index 7793851c..de854ab6 100644 --- a/.github/workflows/conflict.yml +++ b/.github/workflows/conflict.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - name: 🚩 Apply merge conflict label - uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3 + uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0 with: dirtyLabel: '⚔️ merge-conflict' commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.' diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index c6effebf..b0ea48a2 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -1,51 +1,51 @@ -name: 🌐 Translation Sync - -on: - push: - branches: [develop] - paths: - - "translations/**" - - "crowdin.yml" - - "i18n.ts" - - ".github/workflows/crowdin.yml" - # Run weekly to pull new translations - schedule: - - cron: "0 2 * * 1" # Every Monday at 2 AM UTC - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - sync-translations: - runs-on: ubuntu-latest - - steps: - - name: 📥 Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - - - name: 🌐 Sync Translations with Crowdin - uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 - with: - upload_sources: true - upload_translations: true - download_translations: true - localization_branch_name: I10n_crowdin_translations - create_pull_request: true - pull_request_title: "feat: New Crowdin Translations" - pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)" - pull_request_base_branch_name: "develop" - pull_request_labels: "🌐 translation" - # Quality control options - skip_untranslated_strings: false - skip_untranslated_files: false - export_only_approved: false - # Commit customization - commit_message: "feat(i18n): update translations from Crowdin" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} +name: 🌐 Translation Sync + +on: + push: + branches: [develop] + paths: + - "translations/**" + - "crowdin.yml" + - "i18n.ts" + - ".github/workflows/crowdin.yml" + # Run weekly to pull new translations + schedule: + - cron: "0 2 * * 1" # Every Monday at 2 AM UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + sync-translations: + runs-on: ubuntu-latest + + steps: + - name: 📥 Checkout Repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + + - name: 🌐 Sync Translations with Crowdin + uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2 + with: + upload_sources: true + upload_translations: true + download_translations: true + localization_branch_name: I10n_crowdin_translations + create_pull_request: true + pull_request_title: "feat: New Crowdin Translations" + pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)" + pull_request_base_branch_name: "develop" + pull_request_labels: "🌐 translation" + # Quality control options + skip_untranslated_strings: false + skip_untranslated_files: false + export_only_approved: false + # Commit customization + commit_message: "feat(i18n): update translations from Crowdin" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/detect-duplicate.yml b/.github/workflows/detect-duplicate.yml new file mode 100644 index 00000000..265f9efe --- /dev/null +++ b/.github/workflows/detect-duplicate.yml @@ -0,0 +1,38 @@ +name: 🔁 Detect Duplicate Issues + +on: + issues: + types: [opened] + +permissions: + contents: read + +concurrency: + group: detect-duplicate-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + detect: + name: 🔍 Find similar issues + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + permissions: + issues: write + contents: read + steps: + - name: 📥 Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + - name: 🔍 Detect duplicate issues + run: bun scripts/detect-duplicate-issue.mjs + env: + GH_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 50013ba2..8edb8916 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -51,7 +51,7 @@ jobs: contents: read steps: - name: Checkout Repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 @@ -69,7 +69,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: 🛒 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive @@ -97,10 +97,11 @@ jobs: - "check" - "format" - "typecheck" + - "i18n:check" steps: - name: "📥 Checkout PR code" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e9da617..c06e8b34 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,14 @@ -name: 🚀 Release (EAS Build + Submit) +name: 🚀 Release (EAS build + submit) -# Cloud EAS build + auto-submit for iOS, tvOS and Android on merge to main. -# A manual approval gate (the `production` GitHub Environment) pauses the run -# before any build/submit starts. Configure required reviewers on that -# environment in repo Settings → Environments → production. +# On merge to main (gated by the `production` GitHub Environment approval), +# build all targets on EAS in parallel via custom bun build configs: +# 1. iOS phone → App Store (auto-submit) +# 2. tvOS → App Store (auto-submit) +# 3. Android AAB → Google Play (auto-submit) +# 4. Android phone APK→ downloadable artifact +# 5. Android TV APK → downloadable artifact +# Note: EAS queues builds based on your plan's concurrency; parallel jobs +# here just submit them — EAS may still run them serially. concurrency: group: release-${{ github.ref }} @@ -23,7 +28,7 @@ jobs: - name: ✅ Release approved run: echo "Release approved for ${{ github.sha }}" - release: + build: name: 🚀 ${{ matrix.name }} needs: approve runs-on: ubuntu-24.04 @@ -36,16 +41,29 @@ jobs: - name: 🍎 iOS platform: ios profile: production + submit: true - name: 📺 tvOS platform: ios profile: production_tv - - name: 🤖 Android + submit: true + - name: 🤖 Android AAB platform: android profile: production + submit: true + - name: 🤖 Android APK + platform: android + profile: production-apk + submit: false + artifact_name: streamyfin-android-phone-apk + - name: 📺 Android TV APK + platform: android + profile: production-apk-tv + submit: false + artifact_name: streamyfin-android-tv-apk steps: - name: 📥 Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 submodules: recursive @@ -70,16 +88,14 @@ jobs: bun run submodule-reload - name: 🏗️ Setup EAS - uses: expo/expo-github-action@b184ff86a3c926240f1b6db41764c83a01c02eef # main + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main with: eas-version: latest token: ${{ secrets.EXPO_TOKEN }} eas-cache: true - # tvOS uses local credentials (EAS can't manage tvOS provisioning - # remotely, including the TopShelf extension target). Restore the - # gitignored credentials.json + cert + profiles from secrets so the - # cloud build can sign with `credentialsSource: local`. + # tvOS uses credentialsSource: local — restore the gitignored + # credentials.json + cert + provisioning profiles from secrets. - name: 🔐 Restore tvOS signing credentials if: matrix.profile == 'production_tv' env: @@ -94,10 +110,14 @@ jobs: echo "$TVOS_APP_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_tvOS_App_Store.mobileprovision echo "$TVOS_TOPSHELF_PROFILE_BASE64" | base64 -d > profiles/Streamyfin_TopShelf_tvOS_App_Store.mobileprovision - # iOS + tvOS submit upload to App Store Connect with an ASC API key. - # EAS reads it from EXPO_ASC_API_KEY_PATH / EXPO_ASC_KEY_ID / - # EXPO_ASC_ISSUER_ID (set on the build step below). Write the .p8, - # tolerating either raw-PEM or base64-encoded secret content. + # Android Play submit needs the Google Play service account JSON. + - name: 🔐 Restore Google Play service account + if: matrix.platform == 'android' && matrix.submit + env: + GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} + run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json + + # App Store Connect API key for iOS/tvOS submit (raw-PEM or base64). - name: 🔐 Restore App Store Connect API key if: matrix.platform == 'ios' env: @@ -109,18 +129,11 @@ jobs: printf '%s' "$APPLE_KEY_CONTENT" | base64 -d > "$RUNNER_TEMP/asc_api_key.p8" fi - # Android submit needs a Google Play service account JSON. eas.json's - # submit.production.android.serviceAccountKeyPath points at this file. - - name: 🔐 Restore Google Play service account - if: matrix.platform == 'android' - env: - GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }} - run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_KEY" > google-service-account.json - + # ── Submit builds: cloud build + auto-submit to the store ── - name: 🚀 Build & submit (${{ matrix.name }}) + if: matrix.submit env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - # Consumed by eas submit for iOS/tvOS; ignored for Android. EXPO_ASC_API_KEY_PATH: ${{ runner.temp }}/asc_api_key.p8 EXPO_ASC_KEY_ID: ${{ secrets.APPLE_KEY_ID }} EXPO_ASC_ISSUER_ID: ${{ secrets.APPLE_KEY_ISSUER_ID }} @@ -129,4 +142,75 @@ jobs: --platform ${{ matrix.platform }} \ --profile ${{ matrix.profile }} \ --auto-submit \ - --non-interactive + --non-interactive \ + --wait + + # ── Artifact builds: cloud build, then download + upload the APK ── + - name: 🏗️ Build artifact (${{ matrix.name }}) + if: ${{ !matrix.submit }} + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + run: | + eas build \ + --platform ${{ matrix.platform }} \ + --profile ${{ matrix.profile }} \ + --non-interactive \ + --wait \ + --json > build-result.json + URL=$(node -e "const b=require('./build-result.json'); const x=Array.isArray(b)?b[0]:b; console.log(x.artifacts.applicationArchiveUrl)") + echo "Downloading artifact: $URL" + curl -fL "$URL" -o "${{ matrix.artifact_name }}.apk" + + - name: 📤 Upload APK artifact (${{ matrix.name }}) + if: ${{ !matrix.submit }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ matrix.artifact_name }} + path: ${{ matrix.artifact_name }}.apk + retention-days: 14 + + # Draft a GitHub Release with the two APKs attached. The tag comes from the + # merged-in app version (app.json → expo.version), NOT the auto-incremented + # build number — so cutting a release is a deliberate version bump via PR. + github-release: + name: 📦 Draft GitHub Release + needs: build + if: ${{ !cancelled() }} + runs-on: ubuntu-24.04 + permissions: + contents: write + actions: read # required for `gh run download` to list/fetch this run's artifacts + steps: + - name: 📥 Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + show-progress: false + + - name: 📦 Download APK artifacts from this run + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p apks + gh run download ${{ github.run_id }} --name streamyfin-android-phone-apk --dir apks + gh run download ${{ github.run_id }} --name streamyfin-android-tv-apk --dir apks + ls -la apks + + - name: 📝 Draft release (tag = app.json version, not auto-bumped) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION=$(node -e "console.log(require('./app.json').expo.version)") + TAG="v$VERSION" + echo "Release tag from merged app version: $TAG" + if gh release view "$TAG" >/dev/null 2>&1; then + echo "Release $TAG exists — updating APK assets" + gh release upload "$TAG" apks/*.apk --clobber + else + echo "Creating draft release $TAG" + gh release create "$TAG" \ + --draft \ + --generate-notes \ + --title "$TAG" \ + apks/*.apk + fi diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml new file mode 100644 index 00000000..4972e14f --- /dev/null +++ b/.github/workflows/trivy-scan.yml @@ -0,0 +1,60 @@ +name: 🛡️ Trivy Security Scan + +# Filesystem scan (Streamyfin ships no container image): finds vulnerable dependencies, +# leaked secrets and misconfigurations, and reports them to GitHub code scanning. +# Runs post-merge + weekly (not on PRs — dependency-review already gates PRs, and SARIF +# upload needs a write token that fork PRs don't get). +on: + push: + branches: [develop, master] + schedule: + - cron: "50 7 * * 5" # Weekly, Friday 07:50 UTC + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: trivy-${{ github.ref }} + cancel-in-progress: true + +jobs: + trivy: + name: 🔎 Filesystem scan + runs-on: ubuntu-24.04 + permissions: + contents: read + security-events: write # upload SARIF to code scanning + steps: + - name: 📥 Checkout repository + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + # Rotate the DB cache weekly (matches the scheduled scan): cache hits within the week + # instead of a fresh immutable entry per run, still refreshing the DB every week. + - name: 🗓️ Compute weekly Trivy cache key + id: trivy-cache-key + run: echo "value=trivy-db-${{ runner.os }}-$(date -u +%G-%V)" >> "$GITHUB_OUTPUT" + + - name: 💾 Cache Trivy vulnerability DB + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/trivy + key: ${{ steps.trivy-cache-key.outputs.value }} + restore-keys: trivy-db-${{ runner.os }}- + + - name: 🔎 Run Trivy filesystem scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: vuln,secret,misconfig + ignore-unfixed: true + severity: CRITICAL,HIGH + format: sarif + output: trivy-results.sarif + + - name: 📤 Upload results to code scanning + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + sarif_file: trivy-results.sarif + category: trivy-fs diff --git a/.github/workflows/update-issue-form.yml b/.github/workflows/update-issue-form.yml index 7cc32197..a23ecdf2 100644 --- a/.github/workflows/update-issue-form.yml +++ b/.github/workflows/update-issue-form.yml @@ -18,7 +18,7 @@ jobs: steps: - name: 📥 Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: "🟢 Setup Node.js" uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/.gitignore b/.gitignore index 46328035..d46c8a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Dependencies and Package Managers node_modules/ -bun.lock bun.lockb package-lock.json @@ -21,10 +20,8 @@ web-build/ # Gradle caches (top-level + per-module native projects) **/.gradle/ -# Module-specific Builds -modules/mpv-player/android/build -modules/player/android -modules/hls-downloader/android/build +# Native module build outputs (any module) +modules/*/android/build/ # Generated Applications Streamyfin.app @@ -69,13 +66,12 @@ certs/ # Version and Backup Files /version-backup-* -/modules/sf-player/android/build -/modules/music-controls/android/build -modules/background-downloader/android/build/* -/modules/mpv-player/android/build # ios:unsigned-build Artifacts build/ +# but keep EAS custom build configs (the generic build/ rule above matches .eas/build/) +!.eas/build/ +!.eas/build/** .claude/ .agents/skills/** skills-lock.json diff --git a/app.config.js b/app.config.js index 96bbd8ea..d29ddc32 100644 --- a/app.config.js +++ b/app.config.js @@ -1,3 +1,47 @@ +const { execFileSync } = require("node:child_process"); + +// Build metadata, injected into `extra.build` and read at runtime via +// expo-constants (see utils/version.ts). Sources in priority order: +// EAS cloud build → GitHub Actions → explicit EXPO_PUBLIC_* → local git → null. +const git = (args) => { + try { + return execFileSync("git", args, { stdio: ["ignore", "pipe", "ignore"] }) + .toString() + .trim(); + } catch { + return null; + } +}; + +const buildMeta = { + commit: + ( + process.env.EAS_BUILD_GIT_COMMIT_HASH || + process.env.GITHUB_SHA || + process.env.EXPO_PUBLIC_GIT_COMMIT || + git(["rev-parse", "HEAD"]) || + "" + ).slice(0, 7) || null, + branch: + process.env.EAS_BUILD_GIT_BRANCH || + process.env.GITHUB_HEAD_REF || + process.env.GITHUB_REF_NAME || + process.env.EXPO_PUBLIC_GIT_BRANCH || + git(["rev-parse", "--abbrev-ref", "HEAD"]) || + null, + profile: + process.env.EAS_BUILD_PROFILE || + process.env.EXPO_PUBLIC_BUILD_PROFILE || + null, + // GitHub Actions run number (#2098) — lets anyone map a sideloaded CI build back + // to its Actions run (artifacts + logs) without Expo access. Null outside CI. + runNumber: + process.env.GITHUB_RUN_NUMBER || + process.env.EXPO_PUBLIC_GIT_RUN_NUMBER || + null, + builtAt: new Date().toISOString(), +}; + module.exports = ({ config }) => { if (process.env.EXPO_TV !== "1") { config.plugins.push("expo-background-task"); @@ -22,6 +66,8 @@ module.exports = ({ config }) => { androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON; } + config.extra = { ...config.extra, build: buildMeta }; + return { ...(Object.keys(androidConfig).length > 0 && { android: androidConfig }), ...config, diff --git a/app/(auth)/(tabs)/(favorites)/see-all.tsx b/app/(auth)/(tabs)/(favorites)/see-all.tsx index e3b0198a..c885afdc 100644 --- a/app/(auth)/(tabs)/(favorites)/see-all.tsx +++ b/app/(auth)/(tabs)/(favorites)/see-all.tsx @@ -161,9 +161,7 @@ export default function FavoritesSeeAllScreen() { /> {!itemType ? ( - - {t("favorites.noData", { defaultValue: "No items found." })} - + {t("favorites.noData")} ) : isLoading ? ( @@ -194,7 +192,7 @@ export default function FavoritesSeeAllScreen() { ListEmptyComponent={ - {t("home.no_items", { defaultValue: "No items" })} + {t("home.no_items")} } diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index da4a8272..68340e6b 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -137,12 +137,12 @@ export default function DownloadsPage() { deleteFileByType("Episode") .then(() => toast.success( - t("home.downloads.toasts.deleted_all_tvseries_successfully"), + t("home.downloads.toasts.deleted_all_series_successfully"), ), ) .catch((reason) => { writeToLog("ERROR", reason); - toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries")); + toast.error(t("home.downloads.toasts.failed_to_delete_all_series")); }); const deleteOtherMedia = () => Promise.all( @@ -207,7 +207,7 @@ export default function DownloadsPage() { - {t("home.downloads.tvseries")} + {t("home.downloads.series")} @@ -288,7 +288,7 @@ export default function DownloadsPage() { {t("home.downloads.delete_all_movies_button")} {otherMedia.length > 0 && (