From ea1bc2d0713561c4cb1d8aa9b071d9f9ba8e0707 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 20:11:59 +0100 Subject: [PATCH 01/26] fix: use macos-13 and clean packs before workload install --- .github/workflows/buildrelease.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 67893f7..ee2fb16 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -299,7 +299,7 @@ jobs: # ========================================== build-macos: name: Build macOS App - runs-on: macos-14 + runs-on: macos-13 steps: - name: '๐Ÿ“„ Checkout' uses: actions/checkout@v4 @@ -307,23 +307,27 @@ jobs: - name: '๐Ÿ”ง Setup .NET' uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ env.DOTNET_VERSION }} + dotnet-version: '9.0.200' - name: '๐Ÿ” Verify SDK version' run: | dotnet --version dotnet --list-sdks - dotnet workload list - - name: '๐Ÿงน Clean existing workloads' + - name: '๐Ÿงน Clean existing workloads and packs' run: | - # Remove any pre-installed workloads to avoid version conflicts + # Remove any pre-installed workloads and packs to avoid version conflicts dotnet workload clean --all || true + rm -rf ~/.dotnet/packs/Microsoft.MacCatalyst.* || true + rm -rf ~/.dotnet/packs/Microsoft.iOS.* || true + rm -rf ~/.dotnet/metadata/workloads || true - name: '๐ŸŽ Install MAUI workload' run: | dotnet workload install maui-maccatalyst --skip-sign-check dotnet workload list + # Show what SDK packs were installed + ls -la ~/.dotnet/packs/ | grep -i catalyst || true - name: '๐Ÿ“ฆ Extract version' id: version From 6007976596891a74cec9e5437ddb2d081c0fd7de Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 20:16:38 +0100 Subject: [PATCH 02/26] fix: use macos-15 and stable NuGet source --- .github/workflows/buildrelease.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index ee2fb16..2695fd8 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -299,7 +299,7 @@ jobs: # ========================================== build-macos: name: Build macOS App - runs-on: macos-13 + runs-on: macos-15 steps: - name: '๐Ÿ“„ Checkout' uses: actions/checkout@v4 @@ -324,7 +324,8 @@ jobs: - name: '๐ŸŽ Install MAUI workload' run: | - dotnet workload install maui-maccatalyst --skip-sign-check + # Install workload using only stable sources + dotnet workload install maui-maccatalyst --skip-sign-check --source https://api.nuget.org/v3/index.json dotnet workload list # Show what SDK packs were installed ls -la ~/.dotnet/packs/ | grep -i catalyst || true From 83a9bec6dcec76468784d7f7cbf2bb3160676da3 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 20:21:22 +0100 Subject: [PATCH 03/26] fix: nuclear cleanup and rollback file for macOS workloads --- .github/workflows/buildrelease.yml | 38 +++++++++++++++++++----------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 2695fd8..00eaff3 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -304,31 +304,41 @@ jobs: - name: '๐Ÿ“„ Checkout' uses: actions/checkout@v4 - - name: '๐Ÿ”ง Setup .NET' + - name: '๐Ÿ”ง Setup .NET 9.0.100' uses: actions/setup-dotnet@v4 with: - dotnet-version: '9.0.200' + dotnet-version: '9.0.100' - name: '๐Ÿ” Verify SDK version' run: | dotnet --version dotnet --list-sdks - - name: '๐Ÿงน Clean existing workloads and packs' + - name: '๐Ÿงน Clean ALL .NET data' run: | - # Remove any pre-installed workloads and packs to avoid version conflicts - dotnet workload clean --all || true - rm -rf ~/.dotnet/packs/Microsoft.MacCatalyst.* || true - rm -rf ~/.dotnet/packs/Microsoft.iOS.* || true - rm -rf ~/.dotnet/metadata/workloads || true - - - name: '๐ŸŽ Install MAUI workload' + # Nuclear option - remove all workloads, packs, and manifests + rm -rf ~/.dotnet/packs || true + rm -rf ~/.dotnet/metadata || true + rm -rf ~/.dotnet/workloads || true + rm -rf ~/.nuget/packages/microsoft.maui* || true + rm -rf ~/.nuget/packages/microsoft.maccatalyst* || true + rm -rf ~/.nuget/packages/microsoft.ios* || true + + - name: '๐ŸŽ Install MAUI workload with rollback' run: | - # Install workload using only stable sources - dotnet workload install maui-maccatalyst --skip-sign-check --source https://api.nuget.org/v3/index.json + # Create a rollback file to pin workload versions compatible with .NET 9.0.100 + cat > workload-rollback.json << 'EOF' + { + "microsoft.net.sdk.maui": "9.0.0/9.0.100" + } + EOF + + # Try installing with the rollback file + dotnet workload install maui-maccatalyst --skip-sign-check --from-rollback-file workload-rollback.json || \ + dotnet workload install maui-maccatalyst --skip-sign-check + dotnet workload list - # Show what SDK packs were installed - ls -la ~/.dotnet/packs/ | grep -i catalyst || true + ls -la ~/.dotnet/packs/ | grep -i catalyst || echo "No catalyst packs found" - name: '๐Ÿ“ฆ Extract version' id: version From a342187b0c6c9b1b515df9f9b4ee36fc5c926d3a Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 20:26:34 +0100 Subject: [PATCH 04/26] fix: maccatalyst min version 15.0, make builds independent --- .github/workflows/buildrelease.yml | 11 ++++++++++- TFMAudioApp/TFMAudioApp.csproj | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 00eaff3..37f77df 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -412,8 +412,17 @@ jobs: upload-release: name: Upload to Release needs: [build-server, build-android, build-windows, build-macos] + if: always() && needs.build-server.result == 'success' runs-on: ubuntu-latest steps: + - name: '๐Ÿ“Š Build Status Summary' + run: | + echo "Build Results:" + echo " Server: ${{ needs.build-server.result }}" + echo " Android: ${{ needs.build-android.result }}" + echo " Windows: ${{ needs.build-windows.result }}" + echo " macOS: ${{ needs.build-macos.result }}" + - name: '๐Ÿ“ฅ Download all artifacts' uses: actions/download-artifact@v4 with: @@ -422,7 +431,7 @@ jobs: - name: '๐Ÿ“‹ List artifacts' run: | echo "Downloaded artifacts:" - find artifacts -type f -name "*.*" | head -50 + find artifacts -type f -name "*.*" 2>/dev/null | head -50 || echo "No artifacts found" - name: '๐Ÿš€ Upload to GitHub Release' env: diff --git a/TFMAudioApp/TFMAudioApp.csproj b/TFMAudioApp/TFMAudioApp.csproj index 6590c9b..7ce4830 100644 --- a/TFMAudioApp/TFMAudioApp.csproj +++ b/TFMAudioApp/TFMAudioApp.csproj @@ -43,7 +43,7 @@ 24.0 10.0.17763.0 - 14.0 + 15.0 10.0.17763.0 From 604b66b5b6e075c591958ed72cf83069aeddf878 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 20:50:40 +0100 Subject: [PATCH 05/26] feat: separate builds by tag prefix (server-v*, app-v*, v*) --- .github/workflows/buildrelease.yml | 97 ++++++++++++++++++++-- .github/workflows/release-docker-image.yml | 20 +++-- TFMAudioApp/Services/AudioPlayerService.cs | 4 +- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 37f77df..3624616 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -1,8 +1,38 @@ name: Build and Release +# Triggers: +# - Release published with tags: +# - server-v* : Build only Server +# - app-v* : Build only TFMAudioApp (Android, Windows, macOS) +# - v* : Build everything +# - Manual workflow dispatch with checkboxes + on: release: types: [published] + workflow_dispatch: + inputs: + build_server: + description: 'Build Server' + type: boolean + default: false + build_android: + description: 'Build Android App' + type: boolean + default: false + build_windows: + description: 'Build Windows App' + type: boolean + default: false + build_macos: + description: 'Build macOS App' + type: boolean + default: false + version: + description: 'Version (e.g., 1.0.0)' + type: string + required: false + default: '0.0.0-manual' permissions: contents: write @@ -19,6 +49,10 @@ jobs: build-server: name: Build Server runs-on: ubuntu-latest + # Run if: manual with build_server OR release with server-v* or v* (but not app-v*) + if: | + (github.event_name == 'workflow_dispatch' && inputs.build_server) || + (github.event_name == 'release' && (startsWith(github.event.release.tag_name, 'server-v') || (startsWith(github.event.release.tag_name, 'v') && !startsWith(github.event.release.tag_name, 'app-v')))) steps: - name: '๐Ÿ“„ Checkout' uses: actions/checkout@v4 @@ -38,8 +72,15 @@ jobs: - name: '๐Ÿ“ฆ Extract version' id: version run: | - TAG=${{ github.event.release.tag_name }} - VERSION=${TAG#v} + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + TAG="v${VERSION}" + else + TAG=${{ github.event.release.tag_name }} + # Remove server-v or v prefix + VERSION=${TAG#server-v} + VERSION=${VERSION#v} + fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT @@ -85,6 +126,10 @@ jobs: build-android: name: Build Android APK runs-on: ubuntu-latest + # Run if: manual with build_android OR release with app-v* or v* (but not server-v*) + if: | + (github.event_name == 'workflow_dispatch' && inputs.build_android) || + (github.event_name == 'release' && (startsWith(github.event.release.tag_name, 'app-v') || (startsWith(github.event.release.tag_name, 'v') && !startsWith(github.event.release.tag_name, 'server-v')))) steps: - name: '๐Ÿ“„ Checkout' uses: actions/checkout@v4 @@ -109,8 +154,15 @@ jobs: - name: '๐Ÿ“ฆ Extract version' id: version run: | - TAG=${{ github.event.release.tag_name }} - VERSION=${TAG#v} + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + TAG="v${VERSION}" + else + TAG=${{ github.event.release.tag_name }} + # Remove app-v or v prefix + VERSION=${TAG#app-v} + VERSION=${VERSION#v} + fi # Extract version number for Android (must be integer) VERSION_CODE=$(echo $VERSION | sed 's/\.//g' | sed 's/[^0-9]//g') # If empty or starts with 0, use date-based version @@ -195,6 +247,10 @@ jobs: build-windows: name: Build Windows App runs-on: windows-2022 + # Run if: manual with build_windows OR release with app-v* or v* (but not server-v*) + if: | + (github.event_name == 'workflow_dispatch' && inputs.build_windows) || + (github.event_name == 'release' && (startsWith(github.event.release.tag_name, 'app-v') || (startsWith(github.event.release.tag_name, 'v') && !startsWith(github.event.release.tag_name, 'server-v')))) steps: - name: '๐Ÿ“„ Checkout' uses: actions/checkout@v4 @@ -220,8 +276,15 @@ jobs: id: version shell: bash run: | - TAG=${{ github.event.release.tag_name }} - VERSION=${TAG#v} + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + TAG="v${VERSION}" + else + TAG=${{ github.event.release.tag_name }} + # Remove app-v or v prefix + VERSION=${TAG#app-v} + VERSION=${VERSION#v} + fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT @@ -239,6 +302,7 @@ jobs: -o bin/windows-app - name: '๐Ÿ” Sign Windows App (if certificate available)' + continue-on-error: true shell: pwsh env: WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }} @@ -300,6 +364,10 @@ jobs: build-macos: name: Build macOS App runs-on: macos-15 + # Run if: manual with build_macos OR release with app-v* or v* (but not server-v*) + if: | + (github.event_name == 'workflow_dispatch' && inputs.build_macos) || + (github.event_name == 'release' && (startsWith(github.event.release.tag_name, 'app-v') || (startsWith(github.event.release.tag_name, 'v') && !startsWith(github.event.release.tag_name, 'server-v')))) steps: - name: '๐Ÿ“„ Checkout' uses: actions/checkout@v4 @@ -343,8 +411,15 @@ jobs: - name: '๐Ÿ“ฆ Extract version' id: version run: | - TAG=${{ github.event.release.tag_name }} - VERSION=${TAG#v} + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ inputs.version }}" + TAG="v${VERSION}" + else + TAG=${{ github.event.release.tag_name }} + # Remove app-v or v prefix + VERSION=${TAG#app-v} + VERSION=${VERSION#v} + fi echo "version=$VERSION" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT @@ -412,7 +487,11 @@ jobs: upload-release: name: Upload to Release needs: [build-server, build-android, build-windows, build-macos] - if: always() && needs.build-server.result == 'success' + # Run if release event AND at least one build succeeded (not skipped) + if: | + always() && + github.event_name == 'release' && + (needs.build-server.result == 'success' || needs.build-android.result == 'success' || needs.build-windows.result == 'success' || needs.build-macos.result == 'success') runs-on: ubuntu-latest steps: - name: '๐Ÿ“Š Build Status Summary' diff --git a/.github/workflows/release-docker-image.yml b/.github/workflows/release-docker-image.yml index cbe4218..2c529c4 100644 --- a/.github/workflows/release-docker-image.yml +++ b/.github/workflows/release-docker-image.yml @@ -1,14 +1,21 @@ name: Docker Image CI (Release) +# Triggers: +# - Push tags: server-v* OR v* (but NOT app-v*) +# Docker is only built for Server releases + on: push: - tags: [ 'v*.*.*' ] + tags: + - 'v*.*.*' + - 'server-v*.*.*' jobs: build: - runs-on: ubuntu-latest + # Skip if it's an app-only release + if: ${{ !startsWith(github.ref_name, 'app-v') }} steps: - uses: actions/checkout@v4 @@ -16,10 +23,13 @@ jobs: - name: Extract version from tag id: version run: | - # Remove 'v' prefix from tag (v1.2.3 -> 1.2.3) - VERSION=${GITHUB_REF_NAME#v} + TAG=${GITHUB_REF_NAME} + # Remove 'server-v' or 'v' prefix + VERSION=${TAG#server-v} + VERSION=${VERSION#v} echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION" + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION from tag: $TAG" - name: 'Login to GitHub Container Registry' uses: docker/login-action@v3 diff --git a/TFMAudioApp/Services/AudioPlayerService.cs b/TFMAudioApp/Services/AudioPlayerService.cs index ab900b7..a9b57f4 100644 --- a/TFMAudioApp/Services/AudioPlayerService.cs +++ b/TFMAudioApp/Services/AudioPlayerService.cs @@ -17,7 +17,7 @@ public class AudioPlayerService : IAudioPlayerService, IDisposable private readonly Random _random = new(); private LibVLC? _libVLC; - private MediaPlayer? _mediaPlayer; + private LibVLCSharp.Shared.MediaPlayer? _mediaPlayer; private Media? _currentMedia; private System.Timers.Timer? _positionTimer; private List _originalQueue = new(); @@ -105,7 +105,7 @@ public void Initialize() }; // Create media player - _mediaPlayer = new MediaPlayer(_libVLC); + _mediaPlayer = new LibVLCSharp.Shared.MediaPlayer(_libVLC); _mediaPlayer.Volume = (int)(_volume * 100); // Setup event handlers From ad452d9860a8431b30136795c405dc9c8f23b524 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 21:12:21 +0100 Subject: [PATCH 06/26] fix: make Android APK signing conditional on keystore availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Separate AndroidPackageFormat from signing configuration in csproj - AndroidKeyStore=true now only set when ANDROID_KEYSTORE_PATH is provided - Workflow uses step outputs to properly detect signing availability - Unsigned builds explicitly pass -p:AndroidKeyStore=false - Fixes "package appears to be invalid" error when installing unsigned APKs ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/buildrelease.yml | 66 ++++++++++++++++-------------- TFMAudioApp/TFMAudioApp.csproj | 6 ++- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 3624616..692b354 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -173,16 +173,22 @@ jobs: echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT - - name: '๐Ÿ” Decode Keystore' - if: ${{ env.ANDROID_KEYSTORE_BASE64 != '' }} + - name: '๐Ÿ” Setup Android Signing' + id: android-signing env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} run: | - echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > tfmaudio.keystore - echo "ANDROID_KEYSTORE_PATH=$(pwd)/tfmaudio.keystore" >> $GITHUB_ENV + if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then + echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > tfmaudio.keystore + echo "ANDROID_KEYSTORE_PATH=$(pwd)/tfmaudio.keystore" >> $GITHUB_ENV + echo "signing_available=true" >> $GITHUB_OUTPUT + echo "โœ… Keystore configured for signing" + else + echo "signing_available=false" >> $GITHUB_OUTPUT + echo "โš ๏ธ No keystore configured, APK will be unsigned" + fi - - name: '๐Ÿ”จ Build Android APK (Signed)' - if: ${{ env.ANDROID_KEYSTORE_PATH != '' }} + - name: '๐Ÿ”จ Build Android APK' env: ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} @@ -191,31 +197,29 @@ jobs: VERSION=${{ steps.version.outputs.version }} VERSION_CODE=${{ steps.version.outputs.version_code }} - dotnet publish TFMAudioApp/TFMAudioApp.csproj \ - -c Release \ - -f net9.0-android \ - -p:BuildSingleTarget=android \ - -p:ApplicationDisplayVersion=$VERSION \ - -p:ApplicationVersion=$VERSION_CODE \ - -p:AndroidKeyStore=true \ - -p:AndroidSigningKeyStore=$ANDROID_KEYSTORE_PATH \ - -p:AndroidSigningKeyAlias=$ANDROID_KEY_ALIAS \ - -p:AndroidSigningKeyPass=$ANDROID_KEY_PASSWORD \ - -p:AndroidSigningStorePass=$ANDROID_KEYSTORE_PASSWORD - - - name: '๐Ÿ”จ Build Android APK (Unsigned - for testing)' - if: ${{ env.ANDROID_KEYSTORE_PATH == '' }} - run: | - VERSION=${{ steps.version.outputs.version }} - VERSION_CODE=${{ steps.version.outputs.version_code }} - - echo "โš ๏ธ Building unsigned APK (no keystore configured)" - dotnet publish TFMAudioApp/TFMAudioApp.csproj \ - -c Release \ - -f net9.0-android \ - -p:BuildSingleTarget=android \ - -p:ApplicationDisplayVersion=$VERSION \ - -p:ApplicationVersion=$VERSION_CODE + if [ "${{ steps.android-signing.outputs.signing_available }}" == "true" ]; then + echo "๐Ÿ” Building SIGNED APK..." + dotnet publish TFMAudioApp/TFMAudioApp.csproj \ + -c Release \ + -f net9.0-android \ + -p:BuildSingleTarget=android \ + -p:ApplicationDisplayVersion=$VERSION \ + -p:ApplicationVersion=$VERSION_CODE \ + -p:AndroidKeyStore=true \ + -p:AndroidSigningKeyStore=$ANDROID_KEYSTORE_PATH \ + -p:AndroidSigningKeyAlias=$ANDROID_KEY_ALIAS \ + -p:AndroidSigningKeyPass=$ANDROID_KEY_PASSWORD \ + -p:AndroidSigningStorePass=$ANDROID_KEYSTORE_PASSWORD + else + echo "โš ๏ธ Building UNSIGNED APK (testing only)..." + dotnet publish TFMAudioApp/TFMAudioApp.csproj \ + -c Release \ + -f net9.0-android \ + -p:BuildSingleTarget=android \ + -p:ApplicationDisplayVersion=$VERSION \ + -p:ApplicationVersion=$VERSION_CODE \ + -p:AndroidKeyStore=false + fi - name: '๐Ÿ“ฆ Prepare APK for release' run: | diff --git a/TFMAudioApp/TFMAudioApp.csproj b/TFMAudioApp/TFMAudioApp.csproj index 7ce4830..405c46f 100644 --- a/TFMAudioApp/TFMAudioApp.csproj +++ b/TFMAudioApp/TFMAudioApp.csproj @@ -47,9 +47,13 @@ 10.0.17763.0 - + apk + + + + true $(ANDROID_KEYSTORE_PATH) $(ANDROID_KEY_ALIAS) From bc022676f8238cf856e488daec5f6b6103a00dcd Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 21:15:07 +0100 Subject: [PATCH 07/26] fix: collect macOS .pkg files instead of looking for .app bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreatePackage=true generates .pkg installer files, not .app bundles. Updated the packaging step to find and copy .pkg files to releases. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/buildrelease.yml | 35 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 692b354..2594e09 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -453,29 +453,34 @@ jobs: -p:CreatePackage=true \ -o bin/macos-arm64 - - name: '๐Ÿ“ฆ Create macOS ZIPs' + - name: '๐Ÿ“ฆ Prepare macOS packages for release' run: | TAG=${{ steps.version.outputs.tag }} mkdir -p releases - # Package Intel (x64) version - APP_X64=$(find bin/macos-x64 -name "*.app" -type d | head -1) - if [ -n "$APP_X64" ]; then - cd "$(dirname "$APP_X64")" - zip -r "${GITHUB_WORKSPACE}/releases/TFMAudioApp-macOS-Intel-${TAG}.zip" "$(basename "$APP_X64")" - cd "${GITHUB_WORKSPACE}" - echo "โœ… macOS Intel app packaged" + echo "๐Ÿ” Looking for .pkg files..." + find bin -name "*.pkg" -type f 2>/dev/null || echo "No .pkg files found" + + # Copy Intel (x64) .pkg + PKG_X64=$(find bin/macos-x64 -name "*.pkg" -type f | head -1) + if [ -n "$PKG_X64" ]; then + cp "$PKG_X64" "releases/TFMAudioApp-macOS-Intel-${TAG}.pkg" + echo "โœ… macOS Intel package ready" + else + echo "โš ๏ธ No Intel .pkg found" fi - # Package Apple Silicon (arm64) version - APP_ARM64=$(find bin/macos-arm64 -name "*.app" -type d | head -1) - if [ -n "$APP_ARM64" ]; then - cd "$(dirname "$APP_ARM64")" - zip -r "${GITHUB_WORKSPACE}/releases/TFMAudioApp-macOS-AppleSilicon-${TAG}.zip" "$(basename "$APP_ARM64")" - cd "${GITHUB_WORKSPACE}" - echo "โœ… macOS Apple Silicon app packaged" + # Copy Apple Silicon (arm64) .pkg + PKG_ARM64=$(find bin/macos-arm64 -name "*.pkg" -type f | head -1) + if [ -n "$PKG_ARM64" ]; then + cp "$PKG_ARM64" "releases/TFMAudioApp-macOS-AppleSilicon-${TAG}.pkg" + echo "โœ… macOS Apple Silicon package ready" + else + echo "โš ๏ธ No Apple Silicon .pkg found" fi + echo "" + echo "๐Ÿ“ฆ Release files:" ls -la releases/ - name: '๐Ÿ“ค Upload macOS artifact' From 859e54823f309a8de81982dd954195853db08969 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 21:32:30 +0100 Subject: [PATCH 08/26] fix: generate debug keystore when no release keystore is configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android requires ALL APKs to be signed to be installed. When no release keystore secrets are configured, generate a temporary debug keystore using keytool to sign the APK. This fixes "App not installed as package appears to be invalid" error. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/buildrelease.yml | 73 +++++++++++++++++------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 2594e09..4709ad0 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -177,49 +177,58 @@ jobs: id: android-signing env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} run: | - if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then + if [ -n "$ANDROID_KEYSTORE_BASE64" ] && [ -n "$ANDROID_KEY_ALIAS" ]; then + # Use provided keystore from secrets echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > tfmaudio.keystore echo "ANDROID_KEYSTORE_PATH=$(pwd)/tfmaudio.keystore" >> $GITHUB_ENV - echo "signing_available=true" >> $GITHUB_OUTPUT - echo "โœ… Keystore configured for signing" + echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> $GITHUB_ENV + echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> $GITHUB_ENV + echo "ANDROID_KEYSTORE_PASSWORD=$ANDROID_KEYSTORE_PASSWORD" >> $GITHUB_ENV + echo "signing_type=release" >> $GITHUB_OUTPUT + echo "โœ… Release keystore configured" else - echo "signing_available=false" >> $GITHUB_OUTPUT - echo "โš ๏ธ No keystore configured, APK will be unsigned" + # Generate a temporary debug keystore (APKs must be signed to install) + echo "โš ๏ธ No release keystore configured, generating debug keystore..." + keytool -genkeypair \ + -v \ + -keystore debug.keystore \ + -alias debugkey \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -storepass android \ + -keypass android \ + -dname "CN=Debug, OU=Debug, O=Debug, L=Debug, ST=Debug, C=US" + echo "ANDROID_KEYSTORE_PATH=$(pwd)/debug.keystore" >> $GITHUB_ENV + echo "ANDROID_KEY_ALIAS=debugkey" >> $GITHUB_ENV + echo "ANDROID_KEY_PASSWORD=android" >> $GITHUB_ENV + echo "ANDROID_KEYSTORE_PASSWORD=android" >> $GITHUB_ENV + echo "signing_type=debug" >> $GITHUB_OUTPUT + echo "โœ… Debug keystore generated" fi - name: '๐Ÿ”จ Build Android APK' - env: - ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} run: | VERSION=${{ steps.version.outputs.version }} VERSION_CODE=${{ steps.version.outputs.version_code }} + SIGNING_TYPE=${{ steps.android-signing.outputs.signing_type }} - if [ "${{ steps.android-signing.outputs.signing_available }}" == "true" ]; then - echo "๐Ÿ” Building SIGNED APK..." - dotnet publish TFMAudioApp/TFMAudioApp.csproj \ - -c Release \ - -f net9.0-android \ - -p:BuildSingleTarget=android \ - -p:ApplicationDisplayVersion=$VERSION \ - -p:ApplicationVersion=$VERSION_CODE \ - -p:AndroidKeyStore=true \ - -p:AndroidSigningKeyStore=$ANDROID_KEYSTORE_PATH \ - -p:AndroidSigningKeyAlias=$ANDROID_KEY_ALIAS \ - -p:AndroidSigningKeyPass=$ANDROID_KEY_PASSWORD \ - -p:AndroidSigningStorePass=$ANDROID_KEYSTORE_PASSWORD - else - echo "โš ๏ธ Building UNSIGNED APK (testing only)..." - dotnet publish TFMAudioApp/TFMAudioApp.csproj \ - -c Release \ - -f net9.0-android \ - -p:BuildSingleTarget=android \ - -p:ApplicationDisplayVersion=$VERSION \ - -p:ApplicationVersion=$VERSION_CODE \ - -p:AndroidKeyStore=false - fi + echo "๐Ÿ” Building APK with $SIGNING_TYPE signing..." + dotnet publish TFMAudioApp/TFMAudioApp.csproj \ + -c Release \ + -f net9.0-android \ + -p:BuildSingleTarget=android \ + -p:ApplicationDisplayVersion=$VERSION \ + -p:ApplicationVersion=$VERSION_CODE \ + -p:AndroidKeyStore=true \ + -p:AndroidSigningKeyStore=$ANDROID_KEYSTORE_PATH \ + -p:AndroidSigningKeyAlias=$ANDROID_KEY_ALIAS \ + -p:AndroidSigningKeyPass=$ANDROID_KEY_PASSWORD \ + -p:AndroidSigningStorePass=$ANDROID_KEYSTORE_PASSWORD - name: '๐Ÿ“ฆ Prepare APK for release' run: | From 80bbdc2fa43ae826682be58bdc25d6adfb479f3c Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 21:51:20 +0100 Subject: [PATCH 09/26] fix: improve Android APK verification and Windows signing Android: - Add Android SDK tools to PATH for apksigner - Add APK validation (zip integrity check) - Add APK signature verification - Prefer signed APK over unsigned - Add more diagnostic output Windows: - Check both certificate AND password before attempting to sign - Test signing on one file first to validate password - Skip gracefully if password is incorrect - Remove continue-on-error to fail fast on real errors --- .github/workflows/buildrelease.yml | 78 ++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 4709ad0..e3d7a05 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -151,6 +151,13 @@ jobs: - name: '๐Ÿ“ฑ Install Android workload' run: dotnet workload install android maui-android --skip-sign-check + - name: '๐Ÿ”ง Setup Android SDK tools' + run: | + # Add Android SDK build-tools to PATH for apksigner + echo "$ANDROID_HOME/build-tools/34.0.0" >> $GITHUB_PATH + echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> $GITHUB_ENV + ls -la $ANDROID_HOME/build-tools/ || echo "No build-tools found" + - name: '๐Ÿ“ฆ Extract version' id: version run: | @@ -235,15 +242,41 @@ jobs: TAG=${{ steps.version.outputs.tag }} mkdir -p releases - # Find the APK (signed or unsigned) - APK_PATH=$(find TFMAudioApp/bin/Release -name "*.apk" | head -1) + echo "๐Ÿ” Searching for APK files..." + find . -name "*.apk" -type f 2>/dev/null || echo "No APK files found" + + # Find the signed APK (prefer -Signed.apk) + APK_PATH=$(find . -name "*-Signed.apk" -type f | head -1) + + # If no signed APK, look for any APK + if [ -z "$APK_PATH" ]; then + APK_PATH=$(find . -name "*.apk" -type f | head -1) + fi if [ -n "$APK_PATH" ]; then + echo "๐Ÿ“ฆ Found APK: $APK_PATH" + echo "๐Ÿ“Š APK size: $(ls -lh "$APK_PATH" | awk '{print $5}')" + + # Verify APK is a valid zip file (APKs are zip archives) + if unzip -t "$APK_PATH" > /dev/null 2>&1; then + echo "โœ… APK is a valid archive" + else + echo "โš ๏ธ APK may be corrupted (not a valid zip)" + fi + + # Check if APK is signed + if command -v apksigner &> /dev/null; then + apksigner verify "$APK_PATH" && echo "โœ… APK signature valid" || echo "โš ๏ธ APK signature verification failed" + elif command -v jarsigner &> /dev/null; then + jarsigner -verify "$APK_PATH" && echo "โœ… APK signature valid" || echo "โš ๏ธ APK signature verification failed" + fi + cp "$APK_PATH" "releases/TFMAudioApp-${TAG}.apk" echo "โœ… APK prepared: releases/TFMAudioApp-${TAG}.apk" ls -la releases/ else echo "โŒ No APK found!" + find . -type f -name "*.a*" | head -20 exit 1 fi @@ -315,15 +348,15 @@ jobs: -o bin/windows-app - name: '๐Ÿ” Sign Windows App (if certificate available)' - continue-on-error: true shell: pwsh env: WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }} WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }} run: | - # Check if certificate is configured - if ([string]::IsNullOrEmpty($env:WINDOWS_CERT_BASE64)) { - Write-Host "โš ๏ธ No Windows certificate configured, skipping signing" + # Check if certificate AND password are configured + if ([string]::IsNullOrEmpty($env:WINDOWS_CERT_BASE64) -or [string]::IsNullOrEmpty($env:WINDOWS_CERT_PASSWORD)) { + Write-Host "โš ๏ธ Windows certificate or password not configured, skipping signing" + Write-Host " To enable signing, configure WINDOWS_CERT_BASE64 and WINDOWS_CERT_PASSWORD secrets" exit 0 } @@ -337,20 +370,35 @@ jobs: Where-Object { $_.FullName -match "x64" } | Select-Object -First 1 -ExpandProperty FullName - if ($signTool) { - # Sign all EXE and DLL files - Get-ChildItem -Path "bin\windows-app" -Include "*.exe","*.dll" -Recurse | ForEach-Object { - & $signTool sign /f $certPath /p $env:WINDOWS_CERT_PASSWORD /t http://timestamp.digicert.com /fd SHA256 $_.FullName + if (-not $signTool) { + Write-Host "โš ๏ธ SignTool not found, skipping signing" + exit 0 + } + + # Test signing with one file first to validate password + $testFile = Get-ChildItem -Path "bin\windows-app" -Filter "*.exe" -Recurse | Select-Object -First 1 + if ($testFile) { + Write-Host "๐Ÿ” Testing certificate password..." + $result = & $signTool sign /f $certPath /p $env:WINDOWS_CERT_PASSWORD /fd SHA256 $testFile.FullName 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "โš ๏ธ Certificate password appears to be incorrect, skipping signing" + Write-Host " Please verify WINDOWS_CERT_PASSWORD secret is correct" + Remove-Item $certPath -Force -ErrorAction SilentlyContinue + exit 0 } + Write-Host "โœ… Certificate validated, signing remaining files..." + + # Sign remaining EXE and DLL files + Get-ChildItem -Path "bin\windows-app" -Include "*.exe","*.dll" -Recurse | + Where-Object { $_.FullName -ne $testFile.FullName } | + ForEach-Object { + & $signTool sign /f $certPath /p $env:WINDOWS_CERT_PASSWORD /t http://timestamp.digicert.com /fd SHA256 $_.FullName | Out-Null + } Write-Host "โœ… Windows app signed successfully" - } else { - Write-Host "โš ๏ธ SignTool not found, skipping signing" } # Clean up certificate - if (Test-Path $certPath) { - Remove-Item $certPath -Force - } + Remove-Item $certPath -Force -ErrorAction SilentlyContinue - name: '๐Ÿ“ฆ Create Windows ZIP' shell: bash From d42953df9c56e568d10b7748831ac82da2024e8e Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 21:57:52 +0100 Subject: [PATCH 10/26] fix: add keystore verification and improve Windows signing checks --- .github/workflows/buildrelease.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index e3d7a05..ce69ff1 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -190,7 +190,29 @@ jobs: run: | if [ -n "$ANDROID_KEYSTORE_BASE64" ] && [ -n "$ANDROID_KEY_ALIAS" ]; then # Use provided keystore from secrets + echo "๐Ÿ“ฆ Decoding keystore from base64..." echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > tfmaudio.keystore + + # Verify keystore was decoded correctly + KEYSTORE_SIZE=$(stat -c%s tfmaudio.keystore 2>/dev/null || stat -f%z tfmaudio.keystore 2>/dev/null) + echo "๐Ÿ“Š Keystore size: $KEYSTORE_SIZE bytes" + + if [ "$KEYSTORE_SIZE" -lt 100 ]; then + echo "โŒ Keystore appears corrupted (too small). Check ANDROID_KEYSTORE_BASE64 secret." + exit 1 + fi + + # Verify keystore credentials work + echo "๐Ÿ” Verifying keystore credentials..." + if keytool -list -keystore tfmaudio.keystore -storepass "$ANDROID_KEYSTORE_PASSWORD" -alias "$ANDROID_KEY_ALIAS" > /dev/null 2>&1; then + echo "โœ… Keystore credentials verified successfully" + else + echo "โŒ Keystore verification failed. Check passwords and alias." + echo " Trying to list keystore contents for debugging..." + keytool -list -keystore tfmaudio.keystore -storepass "$ANDROID_KEYSTORE_PASSWORD" 2>&1 || true + exit 1 + fi + echo "ANDROID_KEYSTORE_PATH=$(pwd)/tfmaudio.keystore" >> $GITHUB_ENV echo "ANDROID_KEY_ALIAS=$ANDROID_KEY_ALIAS" >> $GITHUB_ENV echo "ANDROID_KEY_PASSWORD=$ANDROID_KEY_PASSWORD" >> $GITHUB_ENV From 6207dd3aa1affe93278fde3d058fcfdfe75802ea Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 22:47:36 +0100 Subject: [PATCH 11/26] fix: prevent false track skip when seeking on network streams - Detect false EndReached events after seek attempts - If seek fails on unbuffered stream, resume playback instead of skipping - Handle seek errors gracefully on network streams - Add more debug logging for seek operations --- TFMAudioApp/Services/AudioPlayerService.cs | 75 ++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/TFMAudioApp/Services/AudioPlayerService.cs b/TFMAudioApp/Services/AudioPlayerService.cs index a9b57f4..c28bc43 100644 --- a/TFMAudioApp/Services/AudioPlayerService.cs +++ b/TFMAudioApp/Services/AudioPlayerService.cs @@ -257,10 +257,41 @@ private void OnStopped(object? sender, EventArgs e) _positionTimer?.Stop(); } + private bool _isSeeking; + private DateTime _lastSeekTime = DateTime.MinValue; + private void OnEndReached(object? sender, EventArgs e) { _positionTimer?.Stop(); + // Check if this is a false "end reached" due to a failed seek on network stream + // If we were seeking recently (within 3 seconds) and position is not near the end, ignore this event + var timeSinceSeek = (DateTime.UtcNow - _lastSeekTime).TotalSeconds; + if (timeSinceSeek < 3 && _mediaPlayer != null) + { + var currentPos = _mediaPlayer.Position; + var length = _mediaPlayer.Length; + var positionMs = currentPos * length; + var remainingMs = length - positionMs; + + // If more than 5 seconds remaining, this is likely a false end-of-stream from failed seek + if (remainingMs > 5000) + { + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Ignoring false EndReached after seek (remaining: {remainingMs}ms)"); + // Try to resume playback + MainThread.BeginInvokeOnMainThread(async () => + { + await Task.Delay(500); + if (_mediaPlayer != null && CurrentTrack != null) + { + System.Diagnostics.Debug.WriteLine("[AudioPlayer] Attempting to resume after false EndReached"); + await PlayAtIndexAsync(CurrentIndex); + } + }); + return; + } + } + // Handle end of track on main thread MainThread.BeginInvokeOnMainThread(async () => { @@ -297,7 +328,25 @@ private void OnError(object? sender, EventArgs e) } System.Diagnostics.Debug.WriteLine($"[AudioPlayer] LibVLC error occurred for track: {CurrentTrack?.FileName ?? "null"}"); - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Error: {errorMsg}, retry count: {_retryCount}"); + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Error: {errorMsg}, retry count: {_retryCount}, isSeeking: {_isSeeking}"); + + // If error occurred during seek on network stream, don't treat it as fatal + // Just ignore the seek and continue playing from current position + var timeSinceSeek = (DateTime.UtcNow - _lastSeekTime).TotalSeconds; + if (timeSinceSeek < 3) + { + System.Diagnostics.Debug.WriteLine("[AudioPlayer] Error during seek - attempting to resume playback"); + MainThread.BeginInvokeOnMainThread(async () => + { + await Task.Delay(500); + if (_mediaPlayer != null && CurrentTrack != null && State != PlaybackState.Playing) + { + // Restart the track from the beginning if we can't seek + await PlayAtIndexAsync(CurrentIndex); + } + }); + return; + } // Retry playback if under max retries (server might have been downloading) if (_retryCount < MaxRetries && CurrentIndex >= 0 && CurrentIndex < Queue.Count) @@ -552,7 +601,10 @@ public async Task SeekAsync(TimeSpan position) { if (_mediaPlayer == null || _mediaPlayer.Length <= 0) return; - // Seeking is now allowed for all tracks (server supports progressive streaming with range requests) + // Mark that we're seeking to detect false EndReached events + _isSeeking = true; + _lastSeekTime = DateTime.UtcNow; + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Seeking to {position}"); try @@ -560,13 +612,28 @@ public async Task SeekAsync(TimeSpan position) await MainThread.InvokeOnMainThreadAsync(() => { var positionRatio = (float)(position.TotalMilliseconds / _mediaPlayer.Length); - _mediaPlayer.Position = Math.Clamp(positionRatio, 0f, 1f); + positionRatio = Math.Clamp(positionRatio, 0f, 0.99f); // Don't seek to exact end + + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Setting position ratio: {positionRatio}"); + _mediaPlayer.Position = positionRatio; }); + + // Wait a bit and check if seek succeeded + await Task.Delay(100); + + if (_mediaPlayer != null) + { + var newPos = _mediaPlayer.Position * _mediaPlayer.Length; + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Position after seek: {newPos}ms"); + } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Seek failed: {ex.Message}"); - // Don't propagate error - just ignore failed seek + } + finally + { + _isSeeking = false; } } From 0f25f272b165e2a2982419c716e74f60d6896942 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sat, 27 Dec 2025 23:01:13 +0100 Subject: [PATCH 12/26] fix: increase LibVLC timeouts for streaming from Telegram - Increase network-caching from 30s to 60s for Telegram download delays - Increase tcp-caching to 60s for slow connections - Add prefetch-buffer-size and prefetch-read-size options - Previous commit: detect false EndReached after seeks --- TFMAudioApp/Services/AudioPlayerService.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/TFMAudioApp/Services/AudioPlayerService.cs b/TFMAudioApp/Services/AudioPlayerService.cs index c28bc43..43c679d 100644 --- a/TFMAudioApp/Services/AudioPlayerService.cs +++ b/TFMAudioApp/Services/AudioPlayerService.cs @@ -87,15 +87,19 @@ public void Initialize() "--verbose=2", // Enable verbose logging for debugging "--no-lua", // Disable Lua scripting "--no-snapshot-preview", // No snapshot previews - "--network-caching=30000", // 30 seconds of network buffer (server may need to download from Telegram) - "--live-caching=30000", // 30 seconds for live streams + "--network-caching=60000", // 60 seconds of network buffer (server may need to download from Telegram) + "--live-caching=60000", // 60 seconds for live streams "--file-caching=10000", // 10 seconds file caching "--http-reconnect", // Auto-reconnect on connection drops "--http-continuous", // Enable continuous stream reading "--sout-mux-caching=5000", // Output muxer caching - "--tcp-caching=30000", // TCP caching for slow connections + "--tcp-caching=60000", // TCP caching for slow connections (increased for Telegram delays) "--clock-jitter=0", // Reduce jitter sensitivity - "--clock-synchro=0" // Disable strict clock sync + "--clock-synchro=0", // Disable strict clock sync + "--http-forward-cookies", // Forward cookies (for session handling) + "--adaptive-logic=highest", // Use highest quality available + "--prefetch-buffer-size=1048576", // 1MB prefetch buffer + "--prefetch-read-size=524288" // 512KB prefetch reads ); // Log LibVLC messages for debugging From 0daeff5eebe61b4b8f0adaa585b99b1974fdabb2 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 10:20:50 +0100 Subject: [PATCH 13/26] chore: improve audio seek --- TFMAudioApp/Services/AudioPlayerService.cs | 79 ++++++-------- .../Mobile/MobileStreamController.cs | 102 +++++++++++++++++- global.json | 7 -- 3 files changed, 128 insertions(+), 60 deletions(-) delete mode 100644 global.json diff --git a/TFMAudioApp/Services/AudioPlayerService.cs b/TFMAudioApp/Services/AudioPlayerService.cs index 43c679d..b8658f1 100644 --- a/TFMAudioApp/Services/AudioPlayerService.cs +++ b/TFMAudioApp/Services/AudioPlayerService.cs @@ -80,32 +80,21 @@ public void Initialize() // Initialize LibVLC core Core.Initialize(); - // Create LibVLC instance with audio-only optimizations and network settings - // Increased timeouts to handle server-side file downloads from Telegram + // Create LibVLC instance with minimal options for stability _libVLC = new LibVLC( - "--no-video", // Disable video for audio-only playback - "--verbose=2", // Enable verbose logging for debugging - "--no-lua", // Disable Lua scripting - "--no-snapshot-preview", // No snapshot previews - "--network-caching=60000", // 60 seconds of network buffer (server may need to download from Telegram) - "--live-caching=60000", // 60 seconds for live streams - "--file-caching=10000", // 10 seconds file caching - "--http-reconnect", // Auto-reconnect on connection drops - "--http-continuous", // Enable continuous stream reading - "--sout-mux-caching=5000", // Output muxer caching - "--tcp-caching=60000", // TCP caching for slow connections (increased for Telegram delays) - "--clock-jitter=0", // Reduce jitter sensitivity - "--clock-synchro=0", // Disable strict clock sync - "--http-forward-cookies", // Forward cookies (for session handling) - "--adaptive-logic=highest", // Use highest quality available - "--prefetch-buffer-size=1048576", // 1MB prefetch buffer - "--prefetch-read-size=524288" // 512KB prefetch reads + "--no-video", // Disable video for audio-only playback + "--quiet", // Minimal logging + "--no-lua", // Disable Lua scripting + "--no-snapshot-preview" // No snapshot previews ); - // Log LibVLC messages for debugging + // Only log errors from LibVLC _libVLC.Log += (s, e) => { - System.Diagnostics.Debug.WriteLine($"[LibVLC] {e.Level}: {e.Message}"); + if (e.Level == LogLevel.Error) + { + System.Diagnostics.Debug.WriteLine($"[LibVLC] {e.Level}: {e.Message}"); + } }; // Create media player @@ -603,41 +592,33 @@ public async Task PreviousAsync() public async Task SeekAsync(TimeSpan position) { - if (_mediaPlayer == null || _mediaPlayer.Length <= 0) return; + if (_mediaPlayer == null || _mediaPlayer.Length <= 0) + { + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] SeekAsync: Cannot seek - player null or length 0"); + return; + } - // Mark that we're seeking to detect false EndReached events - _isSeeking = true; _lastSeekTime = DateTime.UtcNow; + var length = _mediaPlayer.Length; + var isSeekable = _mediaPlayer.IsSeekable; + var posBefore = _mediaPlayer.Position; + var timeBefore = _mediaPlayer.Time; - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Seeking to {position}"); + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] === SEEK START === target={position.TotalSeconds:F1}s, isSeekable={isSeekable}"); - try + await MainThread.InvokeOnMainThreadAsync(() => { - await MainThread.InvokeOnMainThreadAsync(() => - { - var positionRatio = (float)(position.TotalMilliseconds / _mediaPlayer.Length); - positionRatio = Math.Clamp(positionRatio, 0f, 0.99f); // Don't seek to exact end - - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Setting position ratio: {positionRatio}"); - _mediaPlayer.Position = positionRatio; - }); - - // Wait a bit and check if seek succeeded - await Task.Delay(100); + // Try using Time property (milliseconds) - sometimes works better than Position + _mediaPlayer.Time = (long)position.TotalMilliseconds; + }); - if (_mediaPlayer != null) - { - var newPos = _mediaPlayer.Position * _mediaPlayer.Length; - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Position after seek: {newPos}ms"); - } - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] Seek failed: {ex.Message}"); - } - finally + // Check if seek worked + await Task.Delay(200); + if (_mediaPlayer != null) { - _isSeeking = false; + var posAfter = _mediaPlayer.Position; + var timeAfter = _mediaPlayer.Time; + System.Diagnostics.Debug.WriteLine($"[AudioPlayer] === SEEK END === before={timeBefore}ms, after={timeAfter}ms, isSeekable={isSeekable}"); } } diff --git a/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs b/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs index 16c4557..beba63e 100644 --- a/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs +++ b/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs @@ -279,14 +279,16 @@ public async Task StreamAudioByTfmId(string channelId, string tfm return PhysicalFile(filePath, mimeType, name, enableRangeProcessing: true); } - // Calculate what we need to serve + // For initial request without Range, return first chunk as 206 with full size info + // LibVLC will use Content-Range to know the total duration if (!hasRange) { - // No range - start from beginning from = 0; - to = Math.Min(12 * 524288, totalLength - 1); // First ~6MB + to = Math.Min(2 * 1024 * 1024, totalLength - 1); // First 2MB } - else if (to == 0 || to >= totalLength) + + // Handle Range request + if (to == 0 || to >= totalLength) { // Open-ended range to = Math.Min(from + (5 * 524288), totalLength - 1); // ~2.5MB chunk @@ -781,6 +783,98 @@ public async Task DownloadAudioComplete(string channelId, string // (balance between speed and not overwhelming Telegram API) private static readonly SemaphoreSlim _downloadSemaphoreSequential = new(3); + /// + /// Stream file progressively for initial request (no Range header). + /// This ensures LibVLC receives Content-Length = full file size so it knows the duration. + /// The actual data is streamed as it becomes available from cache or Telegram. + /// + private async Task StreamProgressivelyAsync( + string channelId, + BsonFileManagerModel dbFile, + string filePath, + long totalLength, + ProgressiveDownloadInfo downloadInfo) + { + const int chunkSize = 524288; // 512KB chunks + long position = 0; + + try + { + while (position < totalLength) + { + // Check how much is available in cache + long availableBytes = 0; + if (System.IO.File.Exists(filePath)) + { + availableBytes = new FileInfo(filePath).Length; + } + + if (position < availableBytes) + { + // Read from cache + var bytesToRead = (int)Math.Min(chunkSize, availableBytes - position); + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + fs.Seek(position, SeekOrigin.Begin); + var buffer = new byte[bytesToRead]; + var read = await fs.ReadAsync(buffer, 0, bytesToRead); + if (read > 0) + { + await Response.Body.WriteAsync(buffer, 0, read); + await Response.Body.FlushAsync(); + position += read; + } + } + else + { + // Need to get from Telegram + if (!dbFile.MessageId.HasValue) + { + _logger.LogError("File has no MessageId, cannot stream from Telegram"); + break; + } + + var message = await _ts.getMessageFile(channelId, dbFile.MessageId.Value); + if (message == null) + { + _logger.LogError("Message not found in Telegram"); + break; + } + + // Align to 512KB boundary for Telegram + var alignedFrom = (position / chunkSize) * chunkSize; + var downloadLength = Math.Min(chunkSize * 4, totalLength - alignedFrom); // Download 2MB at a time + + var data = await _ts.DownloadFileStream(message, alignedFrom, (int)downloadLength); + + var skipBytes = position - alignedFrom; + var bytesToWrite = (int)Math.Min(data.Length - skipBytes, totalLength - position); + + if (bytesToWrite > 0) + { + await Response.Body.WriteAsync(data, (int)skipBytes, bytesToWrite); + await Response.Body.FlushAsync(); + position += bytesToWrite; + } + } + + // Check if client disconnected + if (HttpContext.RequestAborted.IsCancellationRequested) + { + _logger.LogDebug("Client disconnected during progressive streaming"); + break; + } + } + } + catch (OperationCanceledException) + { + _logger.LogDebug("Progressive streaming cancelled"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during progressive streaming at position {Position}", position); + } + } + private static string GetMimeType(string extension) { return extension.ToLowerInvariant() switch diff --git a/global.json b/global.json deleted file mode 100644 index 40584df..0000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "9.0.100", - "rollForward": "latestMinor", - "allowPrerelease": false - } -} From c91c3a858fcfd272722c124962f4784cda5da2a8 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 11:52:15 +0100 Subject: [PATCH 14/26] chore: fix app --- TFMAudioApp/Services/AudioPlayerService.cs | 29 ++++++---------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/TFMAudioApp/Services/AudioPlayerService.cs b/TFMAudioApp/Services/AudioPlayerService.cs index b8658f1..83ddecc 100644 --- a/TFMAudioApp/Services/AudioPlayerService.cs +++ b/TFMAudioApp/Services/AudioPlayerService.cs @@ -592,34 +592,21 @@ public async Task PreviousAsync() public async Task SeekAsync(TimeSpan position) { - if (_mediaPlayer == null || _mediaPlayer.Length <= 0) - { - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] SeekAsync: Cannot seek - player null or length 0"); - return; - } + if (_mediaPlayer == null) return; - _lastSeekTime = DateTime.UtcNow; var length = _mediaPlayer.Length; - var isSeekable = _mediaPlayer.IsSeekable; - var posBefore = _mediaPlayer.Position; - var timeBefore = _mediaPlayer.Time; + if (length <= 0) return; - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] === SEEK START === target={position.TotalSeconds:F1}s, isSeekable={isSeekable}"); + _lastSeekTime = DateTime.UtcNow; + + // Use Position (0.0 to 1.0 ratio) which is more reliable than Time + var positionRatio = (float)(position.TotalMilliseconds / length); + positionRatio = Math.Clamp(positionRatio, 0f, 0.99f); await MainThread.InvokeOnMainThreadAsync(() => { - // Try using Time property (milliseconds) - sometimes works better than Position - _mediaPlayer.Time = (long)position.TotalMilliseconds; + _mediaPlayer.Position = positionRatio; }); - - // Check if seek worked - await Task.Delay(200); - if (_mediaPlayer != null) - { - var posAfter = _mediaPlayer.Position; - var timeAfter = _mediaPlayer.Time; - System.Diagnostics.Debug.WriteLine($"[AudioPlayer] === SEEK END === before={timeBefore}ms, after={timeAfter}ms, isSeekable={isSeekable}"); - } } public async Task PlayAtIndexAsync(int index) From e5d16dbbaad826415997134abb040c8c908202fe Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 12:27:55 +0100 Subject: [PATCH 15/26] chore: avoid pre-download from server when file is playing --- .../Mobile/MobileStreamController.cs | 121 +----------------- 1 file changed, 6 insertions(+), 115 deletions(-) diff --git a/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs b/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs index beba63e..b0b841a 100644 --- a/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs +++ b/TelegramDownloader/Controllers/Mobile/MobileStreamController.cs @@ -227,7 +227,7 @@ public async Task StreamAudioByTfmId(string channelId, string tfm Directory.CreateDirectory(tempPath); } - // Check if file is completely cached + // Check if file is completely cached - serve directly with full Range support if (System.IO.File.Exists(filePath)) { var fileInfo = new FileInfo(filePath); @@ -238,17 +238,6 @@ public async Task StreamAudioByTfmId(string channelId, string tfm } } - // Start or get background download - var downloadInfo = await _progressiveDownload.StartOrGetDownloadAsync( - cacheFileName, channelId, dbFile, filePath); - - // If download completed while we were waiting, serve the file - if (downloadInfo.IsComplete) - { - _logger.LogDebug("Download completed, serving cached file: {FileName}", name); - return PhysicalFile(filePath, mimeType, name, enableRangeProcessing: true); - } - // Parse range header var rangeHeader = Request.Headers["Range"].ToString(); long from = 0; @@ -266,17 +255,11 @@ public async Task StreamAudioByTfmId(string channelId, string tfm long totalLength = dbFile.Size; - // Check if requested range is available in local cache - var downloadedBytes = downloadInfo.DownloadedBytes; + // Check how much is already cached + long cachedBytes = 0; if (System.IO.File.Exists(filePath)) { - downloadedBytes = Math.Max(downloadedBytes, new System.IO.FileInfo(filePath).Length); - } - - // If no range or range is within cached portion, serve from cache - if (!hasRange && downloadedBytes >= totalLength) - { - return PhysicalFile(filePath, mimeType, name, enableRangeProcessing: true); + cachedBytes = new FileInfo(filePath).Length; } // For initial request without Range, return first chunk as 206 with full size info @@ -295,10 +278,10 @@ public async Task StreamAudioByTfmId(string channelId, string tfm } // Check if range is available locally - if (from < downloadedBytes) + if (from < cachedBytes) { // Part or all of range is in cache - var availableEnd = Math.Min(to, downloadedBytes - 1); + var availableEnd = Math.Min(to, cachedBytes - 1); var length = availableEnd - from + 1; _logger.LogDebug("Serving from cache: bytes {From}-{To} of {Total}", from, availableEnd, totalLength); @@ -783,98 +766,6 @@ public async Task DownloadAudioComplete(string channelId, string // (balance between speed and not overwhelming Telegram API) private static readonly SemaphoreSlim _downloadSemaphoreSequential = new(3); - /// - /// Stream file progressively for initial request (no Range header). - /// This ensures LibVLC receives Content-Length = full file size so it knows the duration. - /// The actual data is streamed as it becomes available from cache or Telegram. - /// - private async Task StreamProgressivelyAsync( - string channelId, - BsonFileManagerModel dbFile, - string filePath, - long totalLength, - ProgressiveDownloadInfo downloadInfo) - { - const int chunkSize = 524288; // 512KB chunks - long position = 0; - - try - { - while (position < totalLength) - { - // Check how much is available in cache - long availableBytes = 0; - if (System.IO.File.Exists(filePath)) - { - availableBytes = new FileInfo(filePath).Length; - } - - if (position < availableBytes) - { - // Read from cache - var bytesToRead = (int)Math.Min(chunkSize, availableBytes - position); - using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - fs.Seek(position, SeekOrigin.Begin); - var buffer = new byte[bytesToRead]; - var read = await fs.ReadAsync(buffer, 0, bytesToRead); - if (read > 0) - { - await Response.Body.WriteAsync(buffer, 0, read); - await Response.Body.FlushAsync(); - position += read; - } - } - else - { - // Need to get from Telegram - if (!dbFile.MessageId.HasValue) - { - _logger.LogError("File has no MessageId, cannot stream from Telegram"); - break; - } - - var message = await _ts.getMessageFile(channelId, dbFile.MessageId.Value); - if (message == null) - { - _logger.LogError("Message not found in Telegram"); - break; - } - - // Align to 512KB boundary for Telegram - var alignedFrom = (position / chunkSize) * chunkSize; - var downloadLength = Math.Min(chunkSize * 4, totalLength - alignedFrom); // Download 2MB at a time - - var data = await _ts.DownloadFileStream(message, alignedFrom, (int)downloadLength); - - var skipBytes = position - alignedFrom; - var bytesToWrite = (int)Math.Min(data.Length - skipBytes, totalLength - position); - - if (bytesToWrite > 0) - { - await Response.Body.WriteAsync(data, (int)skipBytes, bytesToWrite); - await Response.Body.FlushAsync(); - position += bytesToWrite; - } - } - - // Check if client disconnected - if (HttpContext.RequestAborted.IsCancellationRequested) - { - _logger.LogDebug("Client disconnected during progressive streaming"); - break; - } - } - } - catch (OperationCanceledException) - { - _logger.LogDebug("Progressive streaming cancelled"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during progressive streaming at position {Position}", position); - } - } - private static string GetMimeType(string extension) { return extension.ToLowerInvariant() switch From 8f9df57dcc718138f3631dabdfb24559305836d0 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 12:55:00 +0100 Subject: [PATCH 16/26] fix: use macos-14 runner and simplify MAUI workload installation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/buildrelease.yml | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index ce69ff1..b6ef1f0 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -446,7 +446,7 @@ jobs: # ========================================== build-macos: name: Build macOS App - runs-on: macos-15 + runs-on: macos-14 # Run if: manual with build_macos OR release with app-v* or v* (but not server-v*) if: | (github.event_name == 'workflow_dispatch' && inputs.build_macos) || @@ -465,31 +465,17 @@ jobs: dotnet --version dotnet --list-sdks - - name: '๐Ÿงน Clean ALL .NET data' + - name: '๐ŸŽ Install MAUI workload' run: | - # Nuclear option - remove all workloads, packs, and manifests - rm -rf ~/.dotnet/packs || true - rm -rf ~/.dotnet/metadata || true - rm -rf ~/.dotnet/workloads || true - rm -rf ~/.nuget/packages/microsoft.maui* || true - rm -rf ~/.nuget/packages/microsoft.maccatalyst* || true - rm -rf ~/.nuget/packages/microsoft.ios* || true - - - name: '๐ŸŽ Install MAUI workload with rollback' - run: | - # Create a rollback file to pin workload versions compatible with .NET 9.0.100 - cat > workload-rollback.json << 'EOF' - { - "microsoft.net.sdk.maui": "9.0.0/9.0.100" - } - EOF - - # Try installing with the rollback file - dotnet workload install maui-maccatalyst --skip-sign-check --from-rollback-file workload-rollback.json || \ + # Install MAUI workload for maccatalyst dotnet workload install maui-maccatalyst --skip-sign-check + # List installed workloads dotnet workload list - ls -la ~/.dotnet/packs/ | grep -i catalyst || echo "No catalyst packs found" + + # Show Xcode version + echo "Xcode version:" + xcodebuild -version - name: '๐Ÿ“ฆ Extract version' id: version From ba2b7faee4791e620b05882eca733d094c4e20b6 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 12:58:34 +0100 Subject: [PATCH 17/26] fix: use macos-15 with Xcode selection for MAUI build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/buildrelease.yml | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index b6ef1f0..7b61c1c 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -446,7 +446,7 @@ jobs: # ========================================== build-macos: name: Build macOS App - runs-on: macos-14 + runs-on: macos-15 # Run if: manual with build_macos OR release with app-v* or v* (but not server-v*) if: | (github.event_name == 'workflow_dispatch' && inputs.build_macos) || @@ -465,6 +465,20 @@ jobs: dotnet --version dotnet --list-sdks + - name: '๐Ÿ”ง Select Xcode version' + run: | + # List available Xcode versions + echo "Available Xcode versions:" + ls -d /Applications/Xcode*.app 2>/dev/null || echo "No Xcode apps found" + + # Use the latest stable Xcode + sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer || \ + sudo xcode-select -s /Applications/Xcode_16.1.app/Contents/Developer || \ + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + + echo "Selected Xcode:" + xcodebuild -version + - name: '๐ŸŽ Install MAUI workload' run: | # Install MAUI workload for maccatalyst @@ -473,10 +487,6 @@ jobs: # List installed workloads dotnet workload list - # Show Xcode version - echo "Xcode version:" - xcodebuild -version - - name: '๐Ÿ“ฆ Extract version' id: version run: | From 6f03f5e7c7f54314a46631973f92d101db53dc08 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 13:05:39 +0100 Subject: [PATCH 18/26] fix: filter in windows app --- TFMAudioApp/Controls/FilterPopup.xaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TFMAudioApp/Controls/FilterPopup.xaml b/TFMAudioApp/Controls/FilterPopup.xaml index 468f633..2382ef8 100644 --- a/TFMAudioApp/Controls/FilterPopup.xaml +++ b/TFMAudioApp/Controls/FilterPopup.xaml @@ -9,11 +9,13 @@ StrokeThickness="0" Padding="20" MinimumWidthRequest="340" - MaximumWidthRequest="400"> + MaximumWidthRequest="400" + MaximumHeightRequest="600"> + @@ -141,5 +143,6 @@ Clicked="OnApplyClicked"/> + From e0ba1bce3b724043df30f4d77624963ac1ded747 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 13:14:07 +0100 Subject: [PATCH 19/26] fix: pin MAUI workload versions to avoid SDK 26.0 bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/buildrelease.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/buildrelease.yml b/.github/workflows/buildrelease.yml index 7b61c1c..9f89f37 100644 --- a/.github/workflows/buildrelease.yml +++ b/.github/workflows/buildrelease.yml @@ -479,13 +479,26 @@ jobs: echo "Selected Xcode:" xcodebuild -version - - name: '๐ŸŽ Install MAUI workload' + - name: '๐ŸŽ Install MAUI workload with version pinning' run: | - # Install MAUI workload for maccatalyst - dotnet workload install maui-maccatalyst --skip-sign-check + # Create rollback file to pin workload versions compatible with .NET 9.0.100 and Xcode 16.x + cat > rollback.json << 'EOF' + { + "microsoft.net.sdk.maui": "9.0.0/9.0.100", + "microsoft.net.sdk.maccatalyst": "18.0.9600/9.0.100" + } + EOF + + echo "Rollback file contents:" + cat rollback.json + + # Install with rollback file to pin versions + dotnet workload install maui-maccatalyst --skip-sign-check --from-rollback-file rollback.json - # List installed workloads + # List installed workloads and SDKs dotnet workload list + echo "Installed packs:" + ls ~/.dotnet/packs/ | grep -i catalyst || true - name: '๐Ÿ“ฆ Extract version' id: version From 04591bbeb6239a3d61534c8969a7066c717d18bd Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Sun, 28 Dec 2025 14:03:32 +0100 Subject: [PATCH 20/26] feat: add search and filter params to url in server fix: solve add to playlist issue --- TFMAudioApp/Controls/PlaylistPickerPopup.xaml | 23 +---- .../Controls/PlaylistPickerPopup.xaml.cs | 4 +- .../Pages/Partials/impl/FileManagerImpl.razor | 88 ++++++++++++++++--- .../MobileFileManager.razor.cs | 39 +++++++- .../MobileFileManagerEventArgs.cs | 6 ++ 5 files changed, 128 insertions(+), 32 deletions(-) diff --git a/TFMAudioApp/Controls/PlaylistPickerPopup.xaml b/TFMAudioApp/Controls/PlaylistPickerPopup.xaml index 6a3c83d..135d17b 100644 --- a/TFMAudioApp/Controls/PlaylistPickerPopup.xaml +++ b/TFMAudioApp/Controls/PlaylistPickerPopup.xaml @@ -61,8 +61,7 @@ + SelectionMode="None"> - - - - - - - - - - - - - - - - - + + +